1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-05 12:10:55 +00:00

Split the Android platform java logic into an Android library module (lib) and an application module (app).

The application module `app` serves double duties of providing the prebuilt Godot binaries ('android_debug.apk', 'android_release.apk') and the Godot custom build template ('android_source.zip').
This commit is contained in:
fhuya
2019-09-02 17:31:51 -07:00
parent ba854bbc7b
commit 7fabfd402f
141 changed files with 386 additions and 282 deletions

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,297 @@
/*
* 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;
// -- GODOT start --
import java.lang.ref.WeakReference;
// -- GODOT end --
/**
* 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.
*/
// -- GODOT start --
private final MessengerHandlerClient mMsgHandler = new MessengerHandlerClient(this);
final Messenger mMessenger = new Messenger(mMsgHandler);
private static class MessengerHandlerClient extends Handler {
private final WeakReference<Stub> mDownloader;
public MessengerHandlerClient(Stub downloader) {
mDownloader = new WeakReference<>(downloader);
}
@Override
public void handleMessage(Message msg) {
Stub downloader = mDownloader.get();
if (downloader != null) {
downloader.handleMessage(msg);
}
}
}
private 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;
}
}
// -- GODOT end --
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,201 @@
/*
* 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;
// -- GODOT start --
import java.lang.ref.WeakReference;
// -- GODOT end --
/**
* 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;
// -- GODOT start --
private final MessengerHandlerServer mMsgHandler = new MessengerHandlerServer(this);
final Messenger mMessenger = new Messenger(mMsgHandler);
private static class MessengerHandlerServer extends Handler {
private final WeakReference<Stub> mDownloader;
public MessengerHandlerServer(Stub downloader) {
mDownloader = new WeakReference<>(downloader);
}
@Override
public void handleMessage(Message msg) {
Stub downloader = mDownloader.get();
if (downloader != null) {
downloader.handleMessage(msg);
}
}
}
private 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;
}
}
// -- GODOT end --
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,367 @@
/*
* 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.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import android.os.SystemClock;
import android.util.Log;
// -- GODOT start --
//import com.android.vending.expansion.downloader.R;
import org.godotengine.godot.R;
// -- GODOT end --
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 "";
}
// -- GODOT start --
return String.format(Locale.ENGLISH, "%.2f",
(float) overallProgress / (1024.0f * 1024.0f))
+ "MB /" +
String.format(Locale.ENGLISH, "%.2f", (float) overallTotal /
(1024.0f * 1024.0f))
+ "MB";
// -- GODOT end --
}
/**
* 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) {
// -- GODOT start --
return String.format(Locale.ENGLISH, "%.2f", bytesPerMillisecond * 1000 / 1024);
// -- GODOT end --
}
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;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
static public String getSaveFilePath(Context c) {
// This technically existed since Honeycomb, but it is critical
// on KitKat and greater versions since it will create the
// directory if needed
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return c.getObbDir().toString();
} else {
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 Play --- 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;
}
public static final int FS_READABLE = 0;
public static final int FS_DOES_NOT_EXIST = 1;
public static final int FS_CANNOT_READ = 2;
/**
* Helper function to ascertain whether a file can be read.
*
* @param c the app/activity/service context
* @param fileName the name (sans path) of the file to query
* @return true if it does exist, false otherwise
*/
static public int getFileStatus(Context c, String fileName) {
// the file may have been delivered by Play --- let's make sure
// it's the size we expect
File fileForNewFile = new File(Helpers.generateSaveFileName(c, fileName));
int returnValue;
if (fileForNewFile.exists()) {
if (fileForNewFile.canRead()) {
returnValue = FS_READABLE;
} else {
returnValue = FS_CANNOT_READ;
}
} else {
returnValue = FS_DOES_NOT_EXIST;
}
return returnValue;
}
/**
* Helper function to ascertain whether the application has the correct access to the OBB
* directory to allow an OBB file to be written.
*
* @param c the app/activity/service context
* @return true if the application can write an OBB file, false otherwise
*/
static public boolean canWriteOBBFile(Context c) {
String path = getSaveFilePath(c);
File fileForNewFile = new File(path);
boolean canWrite;
if (fileForNewFile.exists()) {
canWrite = fileForNewFile.isDirectory() && fileForNewFile.canWrite();
} else {
canWrite = fileForNewFile.mkdirs();
}
return canWrite;
}
/**
* 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,129 @@
/*
* 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;
// -- GODOT start --
import android.annotation.SuppressLint;
// -- GODOT end --
/**
* 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;
}
@SuppressLint("MissingPermission")
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;
}
@SuppressLint("MissingPermission")
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,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 = "CustomIntentService";
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,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,229 @@
/*
* 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;
// -- GODOT start --
//import com.android.vending.expansion.downloader.R;
import org.godotengine.godot.R;
// -- GODOT end --
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.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.os.Build;
import android.os.Messenger;
import android.support.v4.app.NotificationCompat;
/**
* 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 CharSequence mCurrentTitle;
private IDownloaderClient mClientProxy;
private NotificationCompat.Builder mActiveDownloadBuilder;
private NotificationCompat.Builder mBuilder;
private NotificationCompat.Builder mCurrentBuilder;
private CharSequence mLabel;
private String mCurrentText;
private DownloadProgressInfo mProgressInfo;
private PendingIntent mContentIntent;
static final String LOGTAG = "DownloadNotification";
static final int NOTIFICATION_ID = LOGTAG.hashCode();
public PendingIntent getClientIntent() {
return mContentIntent;
}
public void setClientIntent(PendingIntent clientIntent) {
this.mBuilder.setContentIntent(clientIntent);
this.mActiveDownloadBuilder.setContentIntent(clientIntent);
this.mContentIntent = clientIntent;
}
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;
mCurrentBuilder.setTicker(mLabel + ": " + mCurrentText);
mCurrentBuilder.setSmallIcon(iconResource);
mCurrentBuilder.setContentTitle(mCurrentTitle);
mCurrentBuilder.setContentText(mCurrentText);
if (ongoingEvent) {
mCurrentBuilder.setOngoing(true);
} else {
mCurrentBuilder.setOngoing(false);
mCurrentBuilder.setAutoCancel(true);
}
mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
}
}
@Override
public void onDownloadProgress(DownloadProgressInfo progress) {
mProgressInfo = progress;
if (null != mClientProxy) {
mClientProxy.onDownloadProgress(progress);
}
if (progress.mOverallTotal <= 0) {
// we just show the text
mBuilder.setTicker(mCurrentTitle);
mBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
mBuilder.setContentTitle(mCurrentTitle);
mBuilder.setContentText(mCurrentText);
mCurrentBuilder = mBuilder;
} else {
mActiveDownloadBuilder.setProgress((int) progress.mOverallTotal, (int) progress.mOverallProgress, false);
mActiveDownloadBuilder.setContentText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal));
mActiveDownloadBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
mActiveDownloadBuilder.setTicker(mLabel + ": " + mCurrentText);
mActiveDownloadBuilder.setContentTitle(mLabel);
mActiveDownloadBuilder.setContentInfo(mContext.getString(R.string.time_remaining_notification,
Helpers.getTimeRemaining(progress.mTimeRemaining)));
mCurrentBuilder = mActiveDownloadBuilder;
}
mNotificationManager.notify(NOTIFICATION_ID, mCurrentBuilder.build());
}
/**
* 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);
mActiveDownloadBuilder = new NotificationCompat.Builder(ctx);
mBuilder = new NotificationCompat.Builder(ctx);
// Set Notification category and priorities to something that makes sense for a long
// lived background task.
mActiveDownloadBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
mActiveDownloadBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
mBuilder.setCategory(NotificationCompat.CATEGORY_PROGRESS);
mCurrentBuilder = mBuilder;
}
@Override
public void onServiceConnected(Messenger m) {
}
}

View File

@@ -0,0 +1,852 @@
/*
* Copyright (C) 2015 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 android.content.Context;
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.HttpURLConnection;
import java.net.URL;
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;
}
/**
* Executes the download in a separate thread
*/
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
State state = new State(mInfo, mService);
PowerManager.WakeLock wakeLock = null;
int finalStatus = DownloaderService.STATUS_UNKNOWN_ERROR;
try {
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
// -- GODOT start --
//wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
//wakeLock.acquire();
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "org.godot.game:wakelock");
wakeLock.acquire(20 * 60 * 1000L /*20 minutes*/);
// -- GODOT end --
if (Constants.LOGV) {
Log.v(Constants.TAG, "initiating download for " + mInfo.mFileName);
Log.v(Constants.TAG, " at " + mInfo.mUri);
}
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.
URL url = new URL(state.mRequestUri);
HttpURLConnection request = (HttpURLConnection)url.openConnection();
request.setRequestProperty("User-Agent", userAgent());
try {
executeDownload(state, request);
finished = true;
} catch (RetryDownload exc) {
// fall through
} finally {
request.disconnect();
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;
}
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, HttpURLConnection 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);
int responseCode = sendRequest(state, request);
handleExceptionalStatus(state, innerState, request, responseCode);
if (Constants.LOGV) {
Log.v(Constants.TAG, "received response for " + mInfo.mUri);
}
processResponseHeaders(state, innerState, request);
InputStream entityStream = openResponseEntity(state, request);
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, HttpURLConnection response)
throws StopRequest {
try {
return response.getInputStream();
} 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, HttpURLConnection 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, HttpURLConnection response)
throws StopRequest {
String value = response.getHeaderField("Content-Disposition");
if (value != null) {
innerState.mHeaderContentDisposition = value;
}
value = response.getHeaderField("Content-Location");
if (value != null) {
innerState.mHeaderContentLocation = value;
}
value = response.getHeaderField("ETag");
if (value != null) {
innerState.mHeaderETag = value;
}
String headerTransferEncoding = null;
value = response.getHeaderField("Transfer-Encoding");
if (value != null) {
headerTransferEncoding = value;
}
String headerContentType = null;
value = response.getHeaderField("Content-Type");
if (value != null) {
headerContentType = value;
if (!headerContentType.equals("application/vnd.android.obb")) {
throw new StopRequest(DownloaderService.STATUS_FILE_DELIVERED_INCORRECTLY,
"file delivered with incorrect Mime type");
}
}
if (headerTransferEncoding == null) {
long contentLength = response.getContentLength();
if (value != null) {
// this is always set from Market
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 {
innerState.mHeaderContentLength = Long.toString(contentLength);
}
}
} 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, HttpURLConnection connection, int responseCode)
throws StopRequest, RetryDownload {
if (responseCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
handleServiceUnavailable(state, connection);
}
int expectedStatus = innerState.mContinuingDownload ? 206
: DownloaderService.STATUS_SUCCESS;
if (responseCode != expectedStatus) {
handleOtherStatus(state, innerState, responseCode);
} 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);
}
/**
* Add headers for this download to the HTTP request to allow for resume.
*/
private void addRequestHeaders(InnerState innerState, HttpURLConnection request) {
if (innerState.mContinuingDownload) {
if (innerState.mHeaderETag != null) {
request.setRequestProperty("If-Match", innerState.mHeaderETag);
}
request.setRequestProperty("Range", "bytes=" + innerState.mBytesSoFar + "-");
}
}
/**
* Handle a 503 Service Unavailable status by processing the Retry-After
* header.
*/
private void handleServiceUnavailable(State state, HttpURLConnection connection) throws StopRequest {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "got HTTP response code 503");
}
state.mCountRetry = true;
String retryAfterValue = connection.getHeaderField("Retry-After");
if (retryAfterValue != null) {
try {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Retry-After :" + retryAfterValue);
}
state.mRetryAfter = Integer.parseInt(retryAfterValue);
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 int sendRequest(State state, HttpURLConnection request)
throws StopRequest {
try {
return request.getResponseCode();
} 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,110 @@
/*
* Copyright (C) 2010 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.licensing;
import com.google.android.vending.licensing.util.Base64;
import com.google.android.vending.licensing.util.Base64DecoderException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
/**
* An Obfuscator that uses AES to encrypt data.
*/
public class AESObfuscator implements Obfuscator {
private static final String UTF8 = "UTF-8";
private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final byte[] IV =
{ 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 };
private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|";
private Cipher mEncryptor;
private Cipher mDecryptor;
/**
* @param salt an array of random bytes to use for each (un)obfuscation
* @param applicationId application identifier, e.g. the package name
* @param deviceId device identifier. Use as many sources as possible to
* create this unique identifier.
*/
public AESObfuscator(byte[] salt, String applicationId, String deviceId) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM);
KeySpec keySpec =
new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256);
SecretKey tmp = factory.generateSecret(keySpec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM);
mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV));
mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM);
mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV));
} catch (GeneralSecurityException e) {
// This can't happen on a compatible Android device.
throw new RuntimeException("Invalid environment", e);
}
}
public String obfuscate(String original, String key) {
if (original == null) {
return null;
}
try {
// Header is appended as an integrity check
return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8)));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Invalid environment", e);
} catch (GeneralSecurityException e) {
throw new RuntimeException("Invalid environment", e);
}
}
public String unobfuscate(String obfuscated, String key) throws ValidationException {
if (obfuscated == null) {
return null;
}
try {
String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8);
// Check for presence of header. This serves as a final integrity check, for cases
// where the block size is correct during decryption.
int headerIndex = result.indexOf(header+key);
if (headerIndex != 0) {
throw new ValidationException("Header not found (invalid data or key)" + ":" +
obfuscated);
}
return result.substring(header.length()+key.length(), result.length());
} catch (Base64DecoderException e) {
throw new ValidationException(e.getMessage() + ":" + obfuscated);
} catch (IllegalBlockSizeException e) {
throw new ValidationException(e.getMessage() + ":" + obfuscated);
} catch (BadPaddingException e) {
throw new ValidationException(e.getMessage() + ":" + obfuscated);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Invalid environment", e);
}
}
}

View File

@@ -0,0 +1,414 @@
package com.google.android.vending.licensing;
/*
* 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.
*/
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.google.android.vending.licensing.util.URIQueryDecoder;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
/**
* Default policy. All policy decisions are based off of response data received
* from the licensing service. Specifically, the licensing server sends the
* following information: response validity period, error retry period,
* error retry count and a URL for restoring app access in unlicensed cases.
* <p>
* These values will vary based on the the way the application is configured in
* the Google Play publishing console, such as whether the application is
* marked as free or is within its refund period, as well as how often an
* application is checking with the licensing service.
* <p>
* Developers who need more fine grained control over their application's
* licensing policy should implement a custom Policy.
*/
public class APKExpansionPolicy implements Policy {
private static final String TAG = "APKExpansionPolicy";
private static final String PREFS_FILE = "com.google.android.vending.licensing.APKExpansionPolicy";
private static final String PREF_LAST_RESPONSE = "lastResponse";
private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
private static final String PREF_RETRY_UNTIL = "retryUntil";
private static final String PREF_MAX_RETRIES = "maxRetries";
private static final String PREF_RETRY_COUNT = "retryCount";
private static final String PREF_LICENSING_URL = "licensingUrl";
private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
private static final String DEFAULT_RETRY_UNTIL = "0";
private static final String DEFAULT_MAX_RETRIES = "0";
private static final String DEFAULT_RETRY_COUNT = "0";
private static final long MILLIS_PER_MINUTE = 60 * 1000;
private long mValidityTimestamp;
private long mRetryUntil;
private long mMaxRetries;
private long mRetryCount;
private long mLastResponseTime = 0;
private int mLastResponse;
private String mLicensingUrl;
private PreferenceObfuscator mPreferences;
private Vector<String> mExpansionURLs = new Vector<String>();
private Vector<String> mExpansionFileNames = new Vector<String>();
private Vector<Long> mExpansionFileSizes = new Vector<Long>();
/**
* The design of the protocol supports n files. Currently the market can
* only deliver two files. To accommodate this, we have these two constants,
* but the order is the only relevant thing here.
*/
public static final int MAIN_FILE_URL_INDEX = 0;
public static final int PATCH_FILE_URL_INDEX = 1;
/**
* @param context The context for the current application
* @param obfuscator An obfuscator to be used with preferences.
*/
public APKExpansionPolicy(Context context, Obfuscator obfuscator) {
// Import old values
SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
mPreferences = new PreferenceObfuscator(sp, obfuscator);
mLastResponse = Integer.parseInt(
mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
DEFAULT_VALIDITY_TIMESTAMP));
mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
}
/**
* We call this to guarantee that we fetch a fresh policy from the server.
* This is to be used if the URL is invalid.
*/
public void resetPolicy() {
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY));
setRetryUntil(DEFAULT_RETRY_UNTIL);
setMaxRetries(DEFAULT_MAX_RETRIES);
setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT));
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
mPreferences.commit();
}
/**
* Process a new response from the license server.
* <p>
* This data will be used for computing future policy decisions. The
* following parameters are processed:
* <ul>
* <li>VT: the timestamp that the client should consider the response valid
* until
* <li>GT: the timestamp that the client should ignore retry errors until
* <li>GR: the number of retry errors that the client should ignore
* <li>LU: a deep link URL that can enable access for unlicensed apps (e.g.
* buy app on the Play Store)
* </ul>
*
* @param response the result from validating the server response
* @param rawData the raw server response data
*/
public void processServerResponse(int response,
com.google.android.vending.licensing.ResponseData rawData) {
// Update retry counter
if (response != Policy.RETRY) {
setRetryCount(0);
} else {
setRetryCount(mRetryCount + 1);
}
// Update server policy data
Map<String, String> extras = decodeExtras(rawData);
if (response == Policy.LICENSED) {
mLastResponse = response;
// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
setLicensingUrl(null);
setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE));
Set<String> keys = extras.keySet();
for (String key : keys) {
if (key.equals("VT")) {
setValidityTimestamp(extras.get(key));
} else if (key.equals("GT")) {
setRetryUntil(extras.get(key));
} else if (key.equals("GR")) {
setMaxRetries(extras.get(key));
} else if (key.startsWith("FILE_URL")) {
int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1;
setExpansionURL(index, extras.get(key));
} else if (key.startsWith("FILE_NAME")) {
int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1;
setExpansionFileName(index, extras.get(key));
} else if (key.startsWith("FILE_SIZE")) {
int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1;
setExpansionFileSize(index, Long.parseLong(extras.get(key)));
}
}
} else if (response == Policy.NOT_LICENSED) {
// Clear out stale retry params
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
setRetryUntil(DEFAULT_RETRY_UNTIL);
setMaxRetries(DEFAULT_MAX_RETRIES);
// Update the licensing URL
setLicensingUrl(extras.get("LU"));
}
setLastResponse(response);
mPreferences.commit();
}
/**
* Set the last license response received from the server and add to
* preferences. You must manually call PreferenceObfuscator.commit() to
* commit these changes to disk.
*
* @param l the response
*/
private void setLastResponse(int l) {
mLastResponseTime = System.currentTimeMillis();
mLastResponse = l;
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
}
/**
* Set the current retry count and add to preferences. You must manually
* call PreferenceObfuscator.commit() to commit these changes to disk.
*
* @param c the new retry count
*/
private void setRetryCount(long c) {
mRetryCount = c;
mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
}
public long getRetryCount() {
return mRetryCount;
}
/**
* Set the last validity timestamp (VT) received from the server and add to
* preferences. You must manually call PreferenceObfuscator.commit() to
* commit these changes to disk.
*
* @param validityTimestamp the VT string received
*/
private void setValidityTimestamp(String validityTimestamp) {
Long lValidityTimestamp;
try {
lValidityTimestamp = Long.parseLong(validityTimestamp);
} catch (NumberFormatException e) {
// No response or not parseable, expire in one minute.
Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
validityTimestamp = Long.toString(lValidityTimestamp);
}
mValidityTimestamp = lValidityTimestamp;
mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
}
public long getValidityTimestamp() {
return mValidityTimestamp;
}
/**
* Set the retry until timestamp (GT) received from the server and add to
* preferences. You must manually call PreferenceObfuscator.commit() to
* commit these changes to disk.
*
* @param retryUntil the GT string received
*/
private void setRetryUntil(String retryUntil) {
Long lRetryUntil;
try {
lRetryUntil = Long.parseLong(retryUntil);
} catch (NumberFormatException e) {
// No response or not parseable, expire immediately
Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
retryUntil = "0";
lRetryUntil = 0l;
}
mRetryUntil = lRetryUntil;
mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
}
public long getRetryUntil() {
return mRetryUntil;
}
/**
* Set the max retries value (GR) as received from the server and add to
* preferences. You must manually call PreferenceObfuscator.commit() to
* commit these changes to disk.
*
* @param maxRetries the GR string received
*/
private void setMaxRetries(String maxRetries) {
Long lMaxRetries;
try {
lMaxRetries = Long.parseLong(maxRetries);
} catch (NumberFormatException e) {
// No response or not parseable, expire immediately
Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
maxRetries = "0";
lMaxRetries = 0l;
}
mMaxRetries = lMaxRetries;
mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
}
public long getMaxRetries() {
return mMaxRetries;
}
/**
* Set the licensing URL that displays a Play Store UI for the user to regain app access.
*
* @param url the LU string received
*/
private void setLicensingUrl(String url) {
mLicensingUrl = url;
mPreferences.putString(PREF_LICENSING_URL, url);
}
public String getLicensingUrl() {
return mLicensingUrl;
}
/**
* Gets the count of expansion URLs. Since expansionURLs are not committed
* to preferences, this will return zero if there has been no LVL fetch
* in the current session.
*
* @return the number of expansion URLs. (0,1,2)
*/
public int getExpansionURLCount() {
return mExpansionURLs.size();
}
/**
* Gets the expansion URL. Since these URLs are not committed to
* preferences, this will always return null if there has not been an LVL
* fetch in the current session.
*
* @param index the index of the URL to fetch. This value will be either
* MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
*/
public String getExpansionURL(int index) {
if (index < mExpansionURLs.size()) {
return mExpansionURLs.elementAt(index);
}
return null;
}
/**
* Sets the expansion URL. Expansion URL's are not committed to preferences,
* but are instead intended to be stored when the license response is
* processed by the front-end.
*
* @param index the index of the expansion URL. This value will be either
* MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX
* @param URL the URL to set
*/
public void setExpansionURL(int index, String URL) {
if (index >= mExpansionURLs.size()) {
mExpansionURLs.setSize(index + 1);
}
mExpansionURLs.set(index, URL);
}
public String getExpansionFileName(int index) {
if (index < mExpansionFileNames.size()) {
return mExpansionFileNames.elementAt(index);
}
return null;
}
public void setExpansionFileName(int index, String name) {
if (index >= mExpansionFileNames.size()) {
mExpansionFileNames.setSize(index + 1);
}
mExpansionFileNames.set(index, name);
}
public long getExpansionFileSize(int index) {
if (index < mExpansionFileSizes.size()) {
return mExpansionFileSizes.elementAt(index);
}
return -1;
}
public void setExpansionFileSize(int index, long size) {
if (index >= mExpansionFileSizes.size()) {
mExpansionFileSizes.setSize(index + 1);
}
mExpansionFileSizes.set(index, size);
}
/**
* {@inheritDoc} This implementation allows access if either:<br>
* <ol>
* <li>a LICENSED response was received within the validity period
* <li>a RETRY response was received in the last minute, and we are under
* the RETRY count or in the RETRY period.
* </ol>
*/
public boolean allowAccess() {
long ts = System.currentTimeMillis();
if (mLastResponse == Policy.LICENSED) {
// Check if the LICENSED response occurred within the validity
// timeout.
if (ts <= mValidityTimestamp) {
// Cached LICENSED response is still valid.
return true;
}
} else if (mLastResponse == Policy.RETRY &&
ts < mLastResponseTime + MILLIS_PER_MINUTE) {
// Only allow access if we are within the retry period or we haven't
// used up our
// max retries.
return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
}
return false;
}
private Map<String, String> decodeExtras(
com.google.android.vending.licensing.ResponseData rawData) {
Map<String, String> results = new HashMap<String, String>();
if (rawData == null) {
return results;
}
try {
URI rawExtras = new URI("?" + rawData.extra);
URIQueryDecoder.DecodeQuery(rawExtras, results);
} catch (URISyntaxException e) {
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
}
return results;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2010 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.licensing;
/**
* Allows the developer to limit the number of devices using a single license.
* <p>
* The LICENSED response from the server contains a user identifier unique to
* the &lt;application, user&gt; pair. The developer can send this identifier
* to their own server along with some device identifier (a random number
* generated and stored once per application installation,
* {@link android.telephony.TelephonyManager#getDeviceId getDeviceId},
* {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc).
* The more sources used to identify the device, the harder it will be for an
* attacker to spoof.
* <p>
* The server can look at the &lt;application, user, device id&gt; tuple and
* restrict a user's application license to run on at most 10 different devices
* in a week (for example). We recommend not being too restrictive because a
* user might legitimately have multiple devices or be in the process of
* changing phones. This will catch egregious violations of multiple people
* sharing one license.
*/
public interface DeviceLimiter {
/**
* Checks if this device is allowed to use the given user's license.
*
* @param userId the user whose license the server responded with
* @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs
*/
int isDeviceAllowed(String userId);
}

View File

@@ -0,0 +1,389 @@
/*
* Copyright (C) 2010 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.licensing;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.Settings.Secure;
import android.util.Log;
import com.android.vending.licensing.ILicenseResultListener;
import com.android.vending.licensing.ILicensingService;
import com.google.android.vending.licensing.util.Base64;
import com.google.android.vending.licensing.util.Base64DecoderException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
/**
* Client library for Google Play license verifications.
* <p>
* The LicenseChecker is configured via a {@link Policy} which contains the logic to determine
* whether a user should have access to the application. For example, the Policy can define a
* threshold for allowable number of server or client failures before the library reports the user
* as not having access.
* <p>
* Must also provide the Base64-encoded RSA public key associated with your developer account. The
* public key is obtainable from the publisher site.
*/
public class LicenseChecker implements ServiceConnection {
private static final String TAG = "LicenseChecker";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
// Timeout value (in milliseconds) for calls to service.
private static final int TIMEOUT_MS = 10 * 1000;
private static final SecureRandom RANDOM = new SecureRandom();
private static final boolean DEBUG_LICENSE_ERROR = false;
private ILicensingService mService;
private PublicKey mPublicKey;
private final Context mContext;
private final Policy mPolicy;
/**
* A handler for running tasks on a background thread. We don't want license processing to block
* the UI thread.
*/
private Handler mHandler;
private final String mPackageName;
private final String mVersionCode;
private final Set<LicenseValidator> mChecksInProgress = new HashSet<LicenseValidator>();
private final Queue<LicenseValidator> mPendingChecks = new LinkedList<LicenseValidator>();
/**
* @param context a Context
* @param policy implementation of Policy
* @param encodedPublicKey Base64-encoded RSA public key
* @throws IllegalArgumentException if encodedPublicKey is invalid
*/
public LicenseChecker(Context context, Policy policy, String encodedPublicKey) {
mContext = context;
mPolicy = policy;
mPublicKey = generatePublicKey(encodedPublicKey);
mPackageName = mContext.getPackageName();
mVersionCode = getVersionCode(context, mPackageName);
HandlerThread handlerThread = new HandlerThread("background thread");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
}
/**
* Generates a PublicKey instance from a string containing the Base64-encoded public key.
*
* @param encodedPublicKey Base64-encoded public key
* @throws IllegalArgumentException if encodedPublicKey is invalid
*/
private static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
// This won't happen in an Android-compatible environment.
throw new RuntimeException(e);
} catch (Base64DecoderException e) {
Log.e(TAG, "Could not decode from Base64.");
throw new IllegalArgumentException(e);
} catch (InvalidKeySpecException e) {
Log.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
}
}
/**
* Checks if the user should have access to the app. Binds the service if necessary.
* <p>
* NOTE: This call uses a trivially obfuscated string (base64-encoded). For best security, we
* recommend obfuscating the string that is passed into bindService using another method of your
* own devising.
* <p>
* source string: "com.android.vending.licensing.ILicensingService"
* <p>
*
* @param callback
*/
public synchronized void checkAccess(LicenseCheckerCallback callback) {
// If we have a valid recent LICENSED response, we can skip asking
// Market.
if (mPolicy.allowAccess()) {
Log.i(TAG, "Using cached license response");
callback.allow(Policy.LICENSED);
} else {
LicenseValidator validator = new LicenseValidator(mPolicy, new NullDeviceLimiter(),
callback, generateNonce(), mPackageName, mVersionCode);
if (mService == null) {
Log.i(TAG, "Binding to licensing service.");
try {
boolean bindResult = mContext
.bindService(
new Intent(
new String(
// Base64 encoded -
// com.android.vending.licensing.ILicensingService
// Consider encoding this in another way in your
// code to improve security
Base64.decode(
"Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")))
// As of Android 5.0, implicit
// Service Intents are no longer
// allowed because it's not
// possible for the user to
// participate in disambiguating
// them. This does mean we break
// compatibility with Android
// Cupcake devices with this
// release, since setPackage was
// added in Donut.
.setPackage(
new String(
// Base64
// encoded -
// com.android.vending
Base64.decode(
"Y29tLmFuZHJvaWQudmVuZGluZw=="))),
this, // ServiceConnection.
Context.BIND_AUTO_CREATE);
if (bindResult) {
mPendingChecks.offer(validator);
} else {
Log.e(TAG, "Could not bind to service.");
handleServiceConnectionError(validator);
}
} catch (SecurityException e) {
callback.applicationError(LicenseCheckerCallback.ERROR_MISSING_PERMISSION);
} catch (Base64DecoderException e) {
e.printStackTrace();
}
} else {
mPendingChecks.offer(validator);
runChecks();
}
}
}
/**
* Triggers the last deep link licensing URL returned from the server, which redirects users to a
* page which enables them to gain access to the app. If no such URL is returned by the server, it
* will go to the details page of the app in the Play Store.
*/
public void followLastLicensingUrl(Context context) {
String licensingUrl = mPolicy.getLicensingUrl();
if (licensingUrl == null) {
licensingUrl = "https://play.google.com/store/apps/details?id=" + context.getPackageName();
}
Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(licensingUrl));
context.startActivity(marketIntent);
}
private void runChecks() {
LicenseValidator validator;
while ((validator = mPendingChecks.poll()) != null) {
try {
Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName());
mService.checkLicense(
validator.getNonce(), validator.getPackageName(),
new ResultListener(validator));
mChecksInProgress.add(validator);
} catch (RemoteException e) {
Log.w(TAG, "RemoteException in checkLicense call.", e);
handleServiceConnectionError(validator);
}
}
}
private synchronized void finishCheck(LicenseValidator validator) {
mChecksInProgress.remove(validator);
if (mChecksInProgress.isEmpty()) {
cleanupService();
}
}
private class ResultListener extends ILicenseResultListener.Stub {
private final LicenseValidator mValidator;
private Runnable mOnTimeout;
public ResultListener(LicenseValidator validator) {
mValidator = validator;
mOnTimeout = new Runnable() {
public void run() {
Log.i(TAG, "Check timed out.");
handleServiceConnectionError(mValidator);
finishCheck(mValidator);
}
};
startTimeout();
}
private static final int ERROR_CONTACTING_SERVER = 0x101;
private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
private static final int ERROR_NON_MATCHING_UID = 0x103;
// Runs in IPC thread pool. Post it to the Handler, so we can guarantee
// either this or the timeout runs.
public void verifyLicense(final int responseCode, final String signedData,
final String signature) {
mHandler.post(new Runnable() {
public void run() {
Log.i(TAG, "Received response.");
// Make sure it hasn't already timed out.
if (mChecksInProgress.contains(mValidator)) {
clearTimeout();
mValidator.verify(mPublicKey, responseCode, signedData, signature);
finishCheck(mValidator);
}
if (DEBUG_LICENSE_ERROR) {
boolean logResponse;
String stringError = null;
switch (responseCode) {
case ERROR_CONTACTING_SERVER:
logResponse = true;
stringError = "ERROR_CONTACTING_SERVER";
break;
case ERROR_INVALID_PACKAGE_NAME:
logResponse = true;
stringError = "ERROR_INVALID_PACKAGE_NAME";
break;
case ERROR_NON_MATCHING_UID:
logResponse = true;
stringError = "ERROR_NON_MATCHING_UID";
break;
default:
logResponse = false;
}
if (logResponse) {
String android_id = Secure.getString(mContext.getContentResolver(),
Secure.ANDROID_ID);
Date date = new Date();
Log.d(TAG, "Server Failure: " + stringError);
Log.d(TAG, "Android ID: " + android_id);
Log.d(TAG, "Time: " + date.toGMTString());
}
}
}
});
}
private void startTimeout() {
Log.i(TAG, "Start monitoring timeout.");
mHandler.postDelayed(mOnTimeout, TIMEOUT_MS);
}
private void clearTimeout() {
Log.i(TAG, "Clearing timeout.");
mHandler.removeCallbacks(mOnTimeout);
}
}
public synchronized void onServiceConnected(ComponentName name, IBinder service) {
mService = ILicensingService.Stub.asInterface(service);
runChecks();
}
public synchronized void onServiceDisconnected(ComponentName name) {
// Called when the connection with the service has been
// unexpectedly disconnected. That is, Market crashed.
// If there are any checks in progress, the timeouts will handle them.
Log.w(TAG, "Service unexpectedly disconnected.");
mService = null;
}
/**
* Generates policy response for service connection errors, as a result of disconnections or
* timeouts.
*/
private synchronized void handleServiceConnectionError(LicenseValidator validator) {
mPolicy.processServerResponse(Policy.RETRY, null);
if (mPolicy.allowAccess()) {
validator.getCallback().allow(Policy.RETRY);
} else {
validator.getCallback().dontAllow(Policy.RETRY);
}
}
/** Unbinds service if necessary and removes reference to it. */
private void cleanupService() {
if (mService != null) {
try {
mContext.unbindService(this);
} catch (IllegalArgumentException e) {
// Somehow we've already been unbound. This is a non-fatal
// error.
Log.e(TAG, "Unable to unbind from licensing service (already unbound)");
}
mService = null;
}
}
/**
* Inform the library that the context is about to be destroyed, so that any open connections
* can be cleaned up.
* <p>
* Failure to call this method can result in a crash under certain circumstances, such as during
* screen rotation if an Activity requests the license check or when the user exits the
* application.
*/
public synchronized void onDestroy() {
cleanupService();
mHandler.getLooper().quit();
}
/** Generates a nonce (number used once). */
private int generateNonce() {
return RANDOM.nextInt();
}
/**
* Get version code for the application package name.
*
* @param context
* @param packageName application package name
* @return the version code or empty string if package not found
*/
private static String getVersionCode(Context context, String packageName) {
try {
return String.valueOf(
context.getPackageManager().getPackageInfo(packageName, 0).versionCode);
} catch (NameNotFoundException e) {
Log.e(TAG, "Package not found. could not get version code.");
return "";
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2010 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.licensing;
/**
* Callback for the license checker library.
* <p>
* Upon checking with the Market server and conferring with the {@link Policy},
* the library calls the appropriate callback method to communicate the result.
* <p>
* <b>The callback does not occur in the original checking thread.</b> Your
* application should post to the appropriate handling thread or lock
* accordingly.
* <p>
* The reason that is passed back with allow/dontAllow is the base status handed
* to the policy for allowed/disallowing the license. Policy.RETRY will call
* allow or dontAllow depending on other statistics associated with the policy,
* while in most cases Policy.NOT_LICENSED will call dontAllow and
* Policy.LICENSED will Allow.
*/
public interface LicenseCheckerCallback {
/**
* Allow use. App should proceed as normal.
*
* @param reason Policy.LICENSED or Policy.RETRY typically. (although in
* theory the policy can return Policy.NOT_LICENSED here as well)
*/
public void allow(int reason);
/**
* Don't allow use. App should inform user and take appropriate action.
*
* @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory
* the policy can return Policy.LICENSED here as well ---
* perhaps the call to the LVL took too long, for example)
*/
public void dontAllow(int reason);
/** Application error codes. */
public static final int ERROR_INVALID_PACKAGE_NAME = 1;
public static final int ERROR_NON_MATCHING_UID = 2;
public static final int ERROR_NOT_MARKET_MANAGED = 3;
public static final int ERROR_CHECK_IN_PROGRESS = 4;
public static final int ERROR_INVALID_PUBLIC_KEY = 5;
public static final int ERROR_MISSING_PERMISSION = 6;
/**
* Error in application code. Caller did not call or set up license checker
* correctly. Should be considered fatal.
*/
public void applicationError(int errorCode);
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright (C) 2010 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.licensing;
import com.google.android.vending.licensing.util.Base64;
import com.google.android.vending.licensing.util.Base64DecoderException;
import android.text.TextUtils;
import android.util.Log;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
/**
* Contains data related to a licensing request and methods to verify
* and process the response.
*/
class LicenseValidator {
private static final String TAG = "LicenseValidator";
// Server response codes.
private static final int LICENSED = 0x0;
private static final int NOT_LICENSED = 0x1;
private static final int LICENSED_OLD_KEY = 0x2;
private static final int ERROR_NOT_MARKET_MANAGED = 0x3;
private static final int ERROR_SERVER_FAILURE = 0x4;
private static final int ERROR_OVER_QUOTA = 0x5;
private static final int ERROR_CONTACTING_SERVER = 0x101;
private static final int ERROR_INVALID_PACKAGE_NAME = 0x102;
private static final int ERROR_NON_MATCHING_UID = 0x103;
private final Policy mPolicy;
private final LicenseCheckerCallback mCallback;
private final int mNonce;
private final String mPackageName;
private final String mVersionCode;
private final DeviceLimiter mDeviceLimiter;
LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback,
int nonce, String packageName, String versionCode) {
mPolicy = policy;
mDeviceLimiter = deviceLimiter;
mCallback = callback;
mNonce = nonce;
mPackageName = packageName;
mVersionCode = versionCode;
}
public LicenseCheckerCallback getCallback() {
return mCallback;
}
public int getNonce() {
return mNonce;
}
public String getPackageName() {
return mPackageName;
}
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* Verifies the response from server and calls appropriate callback method.
*
* @param publicKey public key associated with the developer account
* @param responseCode server response code
* @param signedData signed data from server
* @param signature server signature
*/
public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
String userId = null;
// Skip signature check for unsuccessful requests
ResponseData data = null;
if (responseCode == LICENSED || responseCode == NOT_LICENSED ||
responseCode == LICENSED_OLD_KEY) {
// Verify signature.
try {
if (TextUtils.isEmpty(signedData)) {
Log.e(TAG, "Signature verification failed: signedData is empty. " +
"(Device not signed-in to any Google accounts?)");
handleInvalidResponse();
return;
}
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature))) {
Log.e(TAG, "Signature verification failed.");
handleInvalidResponse();
return;
}
} catch (NoSuchAlgorithmException e) {
// This can't happen on an Android compatible device.
throw new RuntimeException(e);
} catch (InvalidKeyException e) {
handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY);
return;
} catch (SignatureException e) {
throw new RuntimeException(e);
} catch (Base64DecoderException e) {
Log.e(TAG, "Could not Base64-decode signature.");
handleInvalidResponse();
return;
}
// Parse and validate response.
try {
data = ResponseData.parse(signedData);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Could not parse response.");
handleInvalidResponse();
return;
}
if (data.responseCode != responseCode) {
Log.e(TAG, "Response codes don't match.");
handleInvalidResponse();
return;
}
if (data.nonce != mNonce) {
Log.e(TAG, "Nonce doesn't match.");
handleInvalidResponse();
return;
}
if (!data.packageName.equals(mPackageName)) {
Log.e(TAG, "Package name doesn't match.");
handleInvalidResponse();
return;
}
if (!data.versionCode.equals(mVersionCode)) {
Log.e(TAG, "Version codes don't match.");
handleInvalidResponse();
return;
}
// Application-specific user identifier.
userId = data.userId;
if (TextUtils.isEmpty(userId)) {
Log.e(TAG, "User identifier is empty.");
handleInvalidResponse();
return;
}
}
switch (responseCode) {
case LICENSED:
case LICENSED_OLD_KEY:
int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
handleResponse(limiterResponse, data);
break;
case NOT_LICENSED:
handleResponse(Policy.NOT_LICENSED, data);
break;
case ERROR_CONTACTING_SERVER:
Log.w(TAG, "Error contacting licensing server.");
handleResponse(Policy.RETRY, data);
break;
case ERROR_SERVER_FAILURE:
Log.w(TAG, "An error has occurred on the licensing server.");
handleResponse(Policy.RETRY, data);
break;
case ERROR_OVER_QUOTA:
Log.w(TAG, "Licensing server is refusing to talk to this device, over quota.");
handleResponse(Policy.RETRY, data);
break;
case ERROR_INVALID_PACKAGE_NAME:
handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PACKAGE_NAME);
break;
case ERROR_NON_MATCHING_UID:
handleApplicationError(LicenseCheckerCallback.ERROR_NON_MATCHING_UID);
break;
case ERROR_NOT_MARKET_MANAGED:
handleApplicationError(LicenseCheckerCallback.ERROR_NOT_MARKET_MANAGED);
break;
default:
Log.e(TAG, "Unknown response code for license check.");
handleInvalidResponse();
}
}
/**
* Confers with policy and calls appropriate callback method.
*
* @param response
* @param rawData
*/
private void handleResponse(int response, ResponseData rawData) {
// Update policy data and increment retry counter (if needed)
mPolicy.processServerResponse(response, rawData);
// Given everything we know, including cached data, ask the policy if we should grant
// access.
if (mPolicy.allowAccess()) {
mCallback.allow(response);
} else {
mCallback.dontAllow(response);
}
}
private void handleApplicationError(int code) {
mCallback.applicationError(code);
}
private void handleInvalidResponse() {
mCallback.dontAllow(Policy.NOT_LICENSED);
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2010 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.licensing;
/**
* A DeviceLimiter that doesn't limit the number of devices that can use a
* given user's license.
* <p>
* Unless you have reason to believe that your application is being pirated
* by multiple users using the same license (signing in to Market as the same
* user), we recommend you use this implementation.
*/
public class NullDeviceLimiter implements DeviceLimiter {
public int isDeviceAllowed(String userId) {
return Policy.LICENSED;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2010 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.licensing;
/**
* Interface used as part of a {@link Policy} to allow application authors to obfuscate
* licensing data that will be stored into a SharedPreferences file.
* <p>
* Any transformation scheme must be reversable. Implementing classes may optionally implement an
* integrity check to further prevent modification to preference data. Implementing classes
* should use device-specific information as a key in the obfuscation algorithm to prevent
* obfuscated preferences from being shared among devices.
*/
public interface Obfuscator {
/**
* Obfuscate a string that is being stored into shared preferences.
*
* @param original The data that is to be obfuscated.
* @param key The key for the data that is to be obfuscated.
* @return A transformed version of the original data.
*/
String obfuscate(String original, String key);
/**
* Undo the transformation applied to data by the obfuscate() method.
*
* @param obfuscated The data that is to be un-obfuscated.
* @param key The key for the data that is to be un-obfuscated.
* @return The original data transformed by the obfuscate() method.
* @throws ValidationException Optionally thrown if a data integrity check fails.
*/
String unobfuscate(String obfuscated, String key) throws ValidationException;
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2010 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.licensing;
/**
* Policy used by {@link LicenseChecker} to determine whether a user should have
* access to the application.
*/
public interface Policy {
/**
* Change these values to make it more difficult for tools to automatically
* strip LVL protection from your APK.
*/
/**
* LICENSED means that the server returned back a valid license response
*/
public static final int LICENSED = 0x0100;
/**
* NOT_LICENSED means that the server returned back a valid license response
* that indicated that the user definitively is not licensed
*/
public static final int NOT_LICENSED = 0x0231;
/**
* RETRY means that the license response was unable to be determined ---
* perhaps as a result of faulty networking
*/
public static final int RETRY = 0x0123;
/**
* Provide results from contact with the license server. Retry counts are
* incremented if the current value of response is RETRY. Results will be
* used for any future policy decisions.
*
* @param response the result from validating the server response
* @param rawData the raw server response data, can be null for RETRY
*/
void processServerResponse(int response, ResponseData rawData);
/**
* Check if the user should be allowed access to the application.
*/
boolean allowAccess();
/**
* Gets the licensing URL returned by the server that can enable access for unlicensed apps (e.g.
* buy app on the Play Store).
*/
String getLicensingUrl();
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2010 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.licensing;
import android.content.SharedPreferences;
import android.util.Log;
/**
* An wrapper for SharedPreferences that transparently performs data obfuscation.
*/
public class PreferenceObfuscator {
private static final String TAG = "PreferenceObfuscator";
private final SharedPreferences mPreferences;
private final Obfuscator mObfuscator;
private SharedPreferences.Editor mEditor;
/**
* Constructor.
*
* @param sp A SharedPreferences instance provided by the system.
* @param o The Obfuscator to use when reading or writing data.
*/
public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) {
mPreferences = sp;
mObfuscator = o;
mEditor = null;
}
public void putString(String key, String value) {
if (mEditor == null) {
mEditor = mPreferences.edit();
// -- GODOT start --
mEditor.apply();
// -- GODOT end --
}
String obfuscatedValue = mObfuscator.obfuscate(value, key);
mEditor.putString(key, obfuscatedValue);
}
public String getString(String key, String defValue) {
String result;
String value = mPreferences.getString(key, null);
if (value != null) {
try {
result = mObfuscator.unobfuscate(value, key);
} catch (ValidationException e) {
// Unable to unobfuscate, data corrupt or tampered
Log.w(TAG, "Validation error while reading preference: " + key);
result = defValue;
}
} else {
// Preference not found
result = defValue;
}
return result;
}
public void commit() {
if (mEditor != null) {
mEditor.commit();
mEditor = null;
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2010 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.licensing;
import android.text.TextUtils;
import java.util.regex.Pattern;
/**
* ResponseData from licensing server.
*/
public class ResponseData {
public int responseCode;
public int nonce;
public String packageName;
public String versionCode;
public String userId;
public long timestamp;
/** Response-specific data. */
public String extra;
/**
* Parses response string into ResponseData.
*
* @param responseData response data string
* @throws IllegalArgumentException upon parsing error
* @return ResponseData object
*/
public static ResponseData parse(String responseData) {
// Must parse out main response data and response-specific data.
int index = responseData.indexOf(':');
String mainData, extraData;
if (-1 == index) {
mainData = responseData;
extraData = "";
} else {
mainData = responseData.substring(0, index);
extraData = index >= responseData.length() ? "" : responseData.substring(index + 1);
}
String[] fields = TextUtils.split(mainData, Pattern.quote("|"));
if (fields.length < 6) {
throw new IllegalArgumentException("Wrong number of fields.");
}
ResponseData data = new ResponseData();
data.extra = extraData;
data.responseCode = Integer.parseInt(fields[0]);
data.nonce = Integer.parseInt(fields[1]);
data.packageName = fields[2];
data.versionCode = fields[3];
// Application-specific user identifier.
data.userId = fields[4];
data.timestamp = Long.parseLong(fields[5]);
return data;
}
@Override
public String toString() {
return TextUtils.join("|", new Object[] {
responseCode, nonce, packageName, versionCode,
userId, timestamp
});
}
}

View File

@@ -0,0 +1,300 @@
/*
* Copyright (C) 2010 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.licensing;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.google.android.vending.licensing.util.URIQueryDecoder;
/**
* Default policy. All policy decisions are based off of response data received
* from the licensing service. Specifically, the licensing server sends the
* following information: response validity period, error retry period,
* error retry count and a URL for restoring app access in unlicensed cases.
* <p>
* These values will vary based on the the way the application is configured in
* the Google Play publishing console, such as whether the application is
* marked as free or is within its refund period, as well as how often an
* application is checking with the licensing service.
* <p>
* Developers who need more fine grained control over their application's
* licensing policy should implement a custom Policy.
*/
public class ServerManagedPolicy implements Policy {
private static final String TAG = "ServerManagedPolicy";
private static final String PREFS_FILE = "com.google.android.vending.licensing.ServerManagedPolicy";
private static final String PREF_LAST_RESPONSE = "lastResponse";
private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp";
private static final String PREF_RETRY_UNTIL = "retryUntil";
private static final String PREF_MAX_RETRIES = "maxRetries";
private static final String PREF_RETRY_COUNT = "retryCount";
private static final String PREF_LICENSING_URL = "licensingUrl";
private static final String DEFAULT_VALIDITY_TIMESTAMP = "0";
private static final String DEFAULT_RETRY_UNTIL = "0";
private static final String DEFAULT_MAX_RETRIES = "0";
private static final String DEFAULT_RETRY_COUNT = "0";
private static final long MILLIS_PER_MINUTE = 60 * 1000;
private long mValidityTimestamp;
private long mRetryUntil;
private long mMaxRetries;
private long mRetryCount;
private long mLastResponseTime = 0;
private int mLastResponse;
private String mLicensingUrl;
private PreferenceObfuscator mPreferences;
/**
* @param context The context for the current application
* @param obfuscator An obfuscator to be used with preferences.
*/
public ServerManagedPolicy(Context context, Obfuscator obfuscator) {
// Import old values
SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
mPreferences = new PreferenceObfuscator(sp, obfuscator);
mLastResponse = Integer.parseInt(
mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)));
mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP,
DEFAULT_VALIDITY_TIMESTAMP));
mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL));
mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES));
mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT));
mLicensingUrl = mPreferences.getString(PREF_LICENSING_URL, null);
}
/**
* Process a new response from the license server.
* <p>
* This data will be used for computing future policy decisions. The
* following parameters are processed:
* <ul>
* <li>VT: the timestamp that the client should consider the response valid
* until
* <li>GT: the timestamp that the client should ignore retry errors until
* <li>GR: the number of retry errors that the client should ignore
* <li>LU: a deep link URL that can enable access for unlicensed apps (e.g.
* buy app on the Play Store)
* </ul>
*
* @param response the result from validating the server response
* @param rawData the raw server response data
*/
public void processServerResponse(int response, ResponseData rawData) {
// Update retry counter
if (response != Policy.RETRY) {
setRetryCount(0);
} else {
setRetryCount(mRetryCount + 1);
}
// Update server policy data
Map<String, String> extras = decodeExtras(rawData);
if (response == Policy.LICENSED) {
mLastResponse = response;
// Reset the licensing URL since it is only applicable for NOT_LICENSED responses.
setLicensingUrl(null);
setValidityTimestamp(extras.get("VT"));
setRetryUntil(extras.get("GT"));
setMaxRetries(extras.get("GR"));
} else if (response == Policy.NOT_LICENSED) {
// Clear out stale retry params
setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP);
setRetryUntil(DEFAULT_RETRY_UNTIL);
setMaxRetries(DEFAULT_MAX_RETRIES);
// Update the licensing URL
setLicensingUrl(extras.get("LU"));
}
setLastResponse(response);
mPreferences.commit();
}
/**
* Set the last license response received from the server and add to
* preferences. You must manually call PreferenceObfuscator.commit() to
* commit these changes to disk.
*
* @param l the response
*/
private void setLastResponse(int l) {
mLastResponseTime = System.currentTimeMillis();
mLastResponse = l;
mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l));
}
/**
* Set the current retry count and add to preferences. You must manually
* call PreferenceObfuscator.commit() to commit these changes to disk.
*
* @param c the new retry count
*/
private void setRetryCount(long c) {
mRetryCount = c;
mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c));
}
public long getRetryCount() {
return mRetryCount;
}
/**
* Set the last validity timestamp (VT) received from the server and add to
* preferences. You must manually call PreferenceObfuscator.commit() to
* commit these changes to disk.
*
* @param validityTimestamp the VT string received
*/
private void setValidityTimestamp(String validityTimestamp) {
Long lValidityTimestamp;
try {
lValidityTimestamp = Long.parseLong(validityTimestamp);
} catch (NumberFormatException e) {
// No response or not parsable, expire in one minute.
Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE;
validityTimestamp = Long.toString(lValidityTimestamp);
}
mValidityTimestamp = lValidityTimestamp;
mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
}
public long getValidityTimestamp() {
return mValidityTimestamp;
}
/**
* Set the retry until timestamp (GT) received from the server and add to
* preferences. You must manually call PreferenceObfuscator.commit() to
* commit these changes to disk.
*
* @param retryUntil the GT string received
*/
private void setRetryUntil(String retryUntil) {
Long lRetryUntil;
try {
lRetryUntil = Long.parseLong(retryUntil);
} catch (NumberFormatException e) {
// No response or not parsable, expire immediately
Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled");
retryUntil = "0";
lRetryUntil = 0l;
}
mRetryUntil = lRetryUntil;
mPreferences.putString(PREF_RETRY_UNTIL, retryUntil);
}
public long getRetryUntil() {
return mRetryUntil;
}
/**
* Set the max retries value (GR) as received from the server and add to
* preferences. You must manually call PreferenceObfuscator.commit() to
* commit these changes to disk.
*
* @param maxRetries the GR string received
*/
private void setMaxRetries(String maxRetries) {
Long lMaxRetries;
try {
lMaxRetries = Long.parseLong(maxRetries);
} catch (NumberFormatException e) {
// No response or not parsable, expire immediately
Log.w(TAG, "Licence retry count (GR) missing, grace period disabled");
maxRetries = "0";
lMaxRetries = 0l;
}
mMaxRetries = lMaxRetries;
mPreferences.putString(PREF_MAX_RETRIES, maxRetries);
}
public long getMaxRetries() {
return mMaxRetries;
}
/**
* Set the license URL value (LU) as received from the server and add to preferences. You must
* manually call PreferenceObfuscator.commit() to commit these changes to disk.
*
* @param url the LU string received
*/
private void setLicensingUrl(String url) {
mLicensingUrl = url;
mPreferences.putString(PREF_LICENSING_URL, url);
}
public String getLicensingUrl() {
return mLicensingUrl;
}
/**
* {@inheritDoc}
*
* This implementation allows access if either:<br>
* <ol>
* <li>a LICENSED response was received within the validity period
* <li>a RETRY response was received in the last minute, and we are under
* the RETRY count or in the RETRY period.
* </ol>
*/
public boolean allowAccess() {
long ts = System.currentTimeMillis();
if (mLastResponse == Policy.LICENSED) {
// Check if the LICENSED response occurred within the validity timeout.
if (ts <= mValidityTimestamp) {
// Cached LICENSED response is still valid.
return true;
}
} else if (mLastResponse == Policy.RETRY &&
ts < mLastResponseTime + MILLIS_PER_MINUTE) {
// Only allow access if we are within the retry period or we haven't used up our
// max retries.
return (ts <= mRetryUntil || mRetryCount <= mMaxRetries);
}
return false;
}
private Map<String, String> decodeExtras(
com.google.android.vending.licensing.ResponseData rawData) {
Map<String, String> results = new HashMap<String, String>();
if (rawData == null) {
return results;
}
try {
URI rawExtras = new URI("?" + rawData.extra);
URIQueryDecoder.DecodeQuery(rawExtras, results);
} catch (URISyntaxException e) {
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
}
return results;
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2010 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.licensing;
import android.util.Log;
import com.google.android.vending.licensing.util.URIQueryDecoder;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
/**
* Non-caching policy. All requests will be sent to the licensing service,
* and no local caching is performed.
* <p>
* Using a non-caching policy ensures that there is no local preference data
* for malicious users to tamper with. As a side effect, applications
* will not be permitted to run while offline. Developers should carefully
* weigh the risks of using this Policy over one which implements caching,
* such as ServerManagedPolicy.
* <p>
* Access to the application is only allowed if a LICENSED response is.
* received. All other responses (including RETRY) will deny access.
*/
public class StrictPolicy implements Policy {
private static final String TAG = "StrictPolicy";
private int mLastResponse;
private String mLicensingUrl;
public StrictPolicy() {
// Set default policy. This will force the application to check the policy on launch.
mLastResponse = Policy.RETRY;
mLicensingUrl = null;
}
/**
* Process a new response from the license server. Since we aren't
* performing any caching, this equates to reading the LicenseResponse.
* Any cache-related ResponseData is ignored, but the licensing URL
* extra is still extracted in cases where the app is unlicensed.
*
* @param response the result from validating the server response
* @param rawData the raw server response data
*/
public void processServerResponse(int response, ResponseData rawData) {
mLastResponse = response;
if (response == Policy.NOT_LICENSED) {
Map<String, String> extras = decodeExtras(rawData);
mLicensingUrl = extras.get("LU");
}
}
/**
* {@inheritDoc}
*
* This implementation allows access if and only if a LICENSED response
* was received the last time the server was contacted.
*/
public boolean allowAccess() {
return (mLastResponse == Policy.LICENSED);
}
public String getLicensingUrl() {
return mLicensingUrl;
}
private Map<String, String> decodeExtras(
com.google.android.vending.licensing.ResponseData rawData) {
Map<String, String> results = new HashMap<String, String>();
if (rawData == null) {
return results;
}
try {
URI rawExtras = new URI("?" + rawData.extra);
URIQueryDecoder.DecodeQuery(rawExtras, results);
} catch (URISyntaxException e) {
Log.w(TAG, "Invalid syntax error while decoding extras data from server.");
}
return results;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2010 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.licensing;
/**
* Indicates that an error occurred while validating the integrity of data managed by an
* {@link Obfuscator}.}
*/
public class ValidationException extends Exception {
public ValidationException() {
super();
}
public ValidationException(String s) {
super(s);
}
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,578 @@
// Portions copyright 2002, Google, Inc.
//
// 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.licensing.util;
// This code was converted from code at http://iharder.sourceforge.net/base64/
// Lots of extraneous features were removed.
/* The original code said:
* <p>
* I am placing this code in the Public Domain. Do with it as you will.
* This software comes with no guarantees or warranties but with
* plenty of well-wishing instead!
* Please visit
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
* periodically to check for updates or to contribute improvements.
* </p>
*
* @author Robert Harder
* @author rharder@usa.net
* @version 1.3
*/
// -- GODOT start --
import org.godotengine.godot.BuildConfig;
// -- GODOT end --
/**
* Base64 converter class. This code is not a full-blown MIME encoder;
* it simply converts binary data to base64 data and back.
*
* <p>Note {@link CharBase64} is a GWT-compatible implementation of this
* class.
*/
public class Base64 {
/** Specify encoding (value is {@code true}). */
public final static boolean ENCODE = true;
/** Specify decoding (value is {@code false}). */
public final static boolean DECODE = false;
/** The equals sign (=) as a byte. */
private final static byte EQUALS_SIGN = (byte) '=';
/** The new line character (\n) as a byte. */
private final static byte NEW_LINE = (byte) '\n';
/**
* The 64 valid Base64 values.
*/
private final static byte[] ALPHABET =
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
(byte) '9', (byte) '+', (byte) '/'};
/**
* The 64 valid web safe Base64 values.
*/
private final static byte[] WEBSAFE_ALPHABET =
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
(byte) '9', (byte) '-', (byte) '_'};
/**
* Translates a Base64 value to either its 6-bit reconstruction value
* or a negative number indicating some other meaning.
**/
private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
62, // Plus sign at decimal 43
-9, -9, -9, // Decimal 44 - 46
63, // Slash at decimal 47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
};
/** The web safe decodabet */
private final static byte[] WEBSAFE_DECODABET =
{-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
62, // Dash '-' sign at decimal 45
-9, -9, // Decimal 46-47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
-9, -9, -9, -9, // Decimal 91-94
63, // Underscore '_' at decimal 95
-9, // Decimal 96
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
};
// Indicates white space in encoding
private final static byte WHITE_SPACE_ENC = -5;
// Indicates equals sign in encoding
private final static byte EQUALS_SIGN_ENC = -1;
/** Defeats instantiation. */
private Base64() {
}
/* ******** E N C O D I N G M E T H O D S ******** */
/**
* Encodes up to three bytes of the array <var>source</var>
* and writes the resulting four Base64 bytes to <var>destination</var>.
* The source and destination arrays can be manipulated
* anywhere along their length by specifying
* <var>srcOffset</var> and <var>destOffset</var>.
* This method does not check to make sure your arrays
* are large enough to accommodate <var>srcOffset</var> + 3 for
* the <var>source</var> array or <var>destOffset</var> + 4 for
* the <var>destination</var> array.
* The actual number of significant bytes in your array is
* given by <var>numSigBytes</var>.
*
* @param source the array to convert
* @param srcOffset the index where conversion begins
* @param numSigBytes the number of significant bytes in your array
* @param destination the array to hold the conversion
* @param destOffset the index where output will be put
* @param alphabet is the encoding alphabet
* @return the <var>destination</var> array
* @since 1.3
*/
private static byte[] encode3to4(byte[] source, int srcOffset,
int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
// 1 2 3
// 01234567890123456789012345678901 Bit position
// --------000000001111111122222222 Array position from threeBytes
// --------| || || || | Six bit groups to index alphabet
// >>18 >>12 >> 6 >> 0 Right shift necessary
// 0x3f 0x3f 0x3f Additional AND
// Create buffer with zero-padding if there are only one or two
// significant bytes passed in the array.
// We have to shift left 24 in order to flush out the 1's that appear
// when Java treats a value as negative that is cast from a byte to an int.
int inBuff =
(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
switch (numSigBytes) {
case 3:
destination[destOffset] = alphabet[(inBuff >>> 18)];
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
return destination;
case 2:
destination[destOffset] = alphabet[(inBuff >>> 18)];
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
destination[destOffset + 3] = EQUALS_SIGN;
return destination;
case 1:
destination[destOffset] = alphabet[(inBuff >>> 18)];
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = EQUALS_SIGN;
destination[destOffset + 3] = EQUALS_SIGN;
return destination;
default:
return destination;
} // end switch
} // end encode3to4
/**
* Encodes a byte array into Base64 notation.
* Equivalent to calling
* {@code encodeBytes(source, 0, source.length)}
*
* @param source The data to convert
* @since 1.4
*/
public static String encode(byte[] source) {
return encode(source, 0, source.length, ALPHABET, true);
}
/**
* Encodes a byte array into web safe Base64 notation.
*
* @param source The data to convert
* @param doPadding is {@code true} to pad result with '=' chars
* if it does not fall on 3 byte boundaries
*/
public static String encodeWebSafe(byte[] source, boolean doPadding) {
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
}
/**
* Encodes a byte array into Base64 notation.
*
* @param source The data to convert
* @param off Offset in array where conversion should begin
* @param len Length of data to convert
* @param alphabet is the encoding alphabet
* @param doPadding is {@code true} to pad result with '=' chars
* if it does not fall on 3 byte boundaries
* @since 1.4
*/
public static String encode(byte[] source, int off, int len, byte[] alphabet,
boolean doPadding) {
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
int outLen = outBuff.length;
// If doPadding is false, set length to truncate '='
// padding characters
while (doPadding == false && outLen > 0) {
if (outBuff[outLen - 1] != '=') {
break;
}
outLen -= 1;
}
return new String(outBuff, 0, outLen);
}
/**
* Encodes a byte array into Base64 notation.
*
* @param source The data to convert
* @param off Offset in array where conversion should begin
* @param len Length of data to convert
* @param alphabet is the encoding alphabet
* @param maxLineLength maximum length of one line.
* @return the BASE64-encoded byte array
*/
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
int maxLineLength) {
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
int len43 = lenDiv3 * 4;
byte[] outBuff = new byte[len43 // Main 4:3
+ (len43 / maxLineLength)]; // New lines
int d = 0;
int e = 0;
int len2 = len - 2;
int lineLength = 0;
for (; d < len2; d += 3, e += 4) {
// The following block of code is the same as
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
// but inlined for faster encoding (~20% improvement)
int inBuff =
((source[d + off] << 24) >>> 8)
| ((source[d + 1 + off] << 24) >>> 16)
| ((source[d + 2 + off] << 24) >>> 24);
outBuff[e] = alphabet[(inBuff >>> 18)];
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
lineLength += 4;
if (lineLength == maxLineLength) {
outBuff[e + 4] = NEW_LINE;
e++;
lineLength = 0;
} // end if: end of line
} // end for: each piece of array
if (d < len) {
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
lineLength += 4;
if (lineLength == maxLineLength) {
// Add a last newline
outBuff[e + 4] = NEW_LINE;
e++;
}
e += 4;
}
// -- GODOT start --
//assert (e == outBuff.length);
if (BuildConfig.DEBUG && e != outBuff.length)
throw new RuntimeException();
// -- GODOT end --
return outBuff;
}
/* ******** D E C O D I N G M E T H O D S ******** */
/**
* Decodes four bytes from array <var>source</var>
* and writes the resulting bytes (up to three of them)
* to <var>destination</var>.
* The source and destination arrays can be manipulated
* anywhere along their length by specifying
* <var>srcOffset</var> and <var>destOffset</var>.
* This method does not check to make sure your arrays
* are large enough to accommodate <var>srcOffset</var> + 4 for
* the <var>source</var> array or <var>destOffset</var> + 3 for
* the <var>destination</var> array.
* This method returns the actual number of bytes that
* were converted from the Base64 encoding.
*
*
* @param source the array to convert
* @param srcOffset the index where conversion begins
* @param destination the array to hold the conversion
* @param destOffset the index where output will be put
* @param decodabet the decodabet for decoding Base64 content
* @return the number of decoded bytes converted
* @since 1.3
*/
private static int decode4to3(byte[] source, int srcOffset,
byte[] destination, int destOffset, byte[] decodabet) {
// Example: Dk==
if (source[srcOffset + 2] == EQUALS_SIGN) {
int outBuff =
((decodabet[source[srcOffset]] << 24) >>> 6)
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
destination[destOffset] = (byte) (outBuff >>> 16);
return 1;
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
// Example: DkL=
int outBuff =
((decodabet[source[srcOffset]] << 24) >>> 6)
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
destination[destOffset] = (byte) (outBuff >>> 16);
destination[destOffset + 1] = (byte) (outBuff >>> 8);
return 2;
} else {
// Example: DkLE
int outBuff =
((decodabet[source[srcOffset]] << 24) >>> 6)
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
| ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
destination[destOffset] = (byte) (outBuff >> 16);
destination[destOffset + 1] = (byte) (outBuff >> 8);
destination[destOffset + 2] = (byte) (outBuff);
return 3;
}
} // end decodeToBytes
/**
* Decodes data from Base64 notation.
*
* @param s the string to decode (decoded in default encoding)
* @return the decoded data
* @since 1.4
*/
public static byte[] decode(String s) throws Base64DecoderException {
byte[] bytes = s.getBytes();
return decode(bytes, 0, bytes.length);
}
/**
* Decodes data from web safe Base64 notation.
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
*
* @param s the string to decode (decoded in default encoding)
* @return the decoded data
*/
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
byte[] bytes = s.getBytes();
return decodeWebSafe(bytes, 0, bytes.length);
}
/**
* Decodes Base64 content in byte array format and returns
* the decoded byte array.
*
* @param source The Base64 encoded data
* @return decoded data
* @since 1.3
* @throws Base64DecoderException
*/
public static byte[] decode(byte[] source) throws Base64DecoderException {
return decode(source, 0, source.length);
}
/**
* Decodes web safe Base64 content in byte array format and returns
* the decoded data.
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
*
* @param source the string to decode (decoded in default encoding)
* @return the decoded data
*/
public static byte[] decodeWebSafe(byte[] source)
throws Base64DecoderException {
return decodeWebSafe(source, 0, source.length);
}
/**
* Decodes Base64 content in byte array format and returns
* the decoded byte array.
*
* @param source The Base64 encoded data
* @param off The offset of where to begin decoding
* @param len The length of characters to decode
* @return decoded data
* @since 1.3
* @throws Base64DecoderException
*/
public static byte[] decode(byte[] source, int off, int len)
throws Base64DecoderException {
return decode(source, off, len, DECODABET);
}
/**
* Decodes web safe Base64 content in byte array format and returns
* the decoded byte array.
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
*
* @param source The Base64 encoded data
* @param off The offset of where to begin decoding
* @param len The length of characters to decode
* @return decoded data
*/
public static byte[] decodeWebSafe(byte[] source, int off, int len)
throws Base64DecoderException {
return decode(source, off, len, WEBSAFE_DECODABET);
}
/**
* Decodes Base64 content using the supplied decodabet and returns
* the decoded byte array.
*
* @param source The Base64 encoded data
* @param off The offset of where to begin decoding
* @param len The length of characters to decode
* @param decodabet the decodabet for decoding Base64 content
* @return decoded data
*/
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
throws Base64DecoderException {
int len34 = len * 3 / 4;
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
int outBuffPosn = 0;
byte[] b4 = new byte[4];
int b4Posn = 0;
int i = 0;
byte sbiCrop = 0;
byte sbiDecode = 0;
for (i = 0; i < len; i++) {
sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
sbiDecode = decodabet[sbiCrop];
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
if (sbiDecode >= EQUALS_SIGN_ENC) {
// An equals sign (for padding) must not occur at position 0 or 1
// and must be the last byte[s] in the encoded value
if (sbiCrop == EQUALS_SIGN) {
int bytesLeft = len - i;
byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
if (b4Posn == 0 || b4Posn == 1) {
throw new Base64DecoderException(
"invalid padding byte '=' at byte offset " + i);
} else if ((b4Posn == 3 && bytesLeft > 2)
|| (b4Posn == 4 && bytesLeft > 1)) {
throw new Base64DecoderException(
"padding byte '=' falsely signals end of encoded value "
+ "at offset " + i);
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
throw new Base64DecoderException(
"encoded value has invalid trailing byte");
}
break;
}
b4[b4Posn++] = sbiCrop;
if (b4Posn == 4) {
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
b4Posn = 0;
}
}
} else {
throw new Base64DecoderException("Bad Base64 input character at " + i
+ ": " + source[i + off] + "(decimal)");
}
}
// Because web safe encoding allows non padding base64 encodes, we
// need to pad the rest of the b4 buffer with equal signs when
// b4Posn != 0. There can be at most 2 equal signs at the end of
// four characters, so the b4 buffer must have two or three
// characters. This also catches the case where the input is
// padded with EQUALS_SIGN
if (b4Posn != 0) {
if (b4Posn == 1) {
throw new Base64DecoderException("single trailing character at offset "
+ (len - 1));
}
b4[b4Posn++] = EQUALS_SIGN;
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
}
byte[] out = new byte[outBuffPosn];
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
return out;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright 2002, Google, Inc.
//
// 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.licensing.util;
/**
* Exception thrown when encountering an invalid Base64 input character.
*
* @author nelson
*/
public class Base64DecoderException extends Exception {
public Base64DecoderException() {
super();
}
public Base64DecoderException(String s) {
super(s);
}
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2016 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.licensing.util;
import android.util.Log;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.util.Map;
import java.util.Scanner;
public class URIQueryDecoder {
private static final String TAG = "URIQueryDecoder";
/**
* Decodes the query portion of the passed-in URI.
*
* @param encodedURI the URI containing the query to decode
* @param results a map containing all query parameters. Query parameters that do not have a
* value will map to a null string
*/
static public void DecodeQuery(URI encodedURI, Map<String, String> results) {
Scanner scanner = new Scanner(encodedURI.getRawQuery());
scanner.useDelimiter("&");
try {
while (scanner.hasNext()) {
String param = scanner.next();
String[] valuePair = param.split("=");
String name, value;
if (valuePair.length == 1) {
value = null;
} else if (valuePair.length == 2) {
value = URLDecoder.decode(valuePair[1], "UTF-8");
} else {
throw new IllegalArgumentException("query parameter invalid");
}
name = URLDecoder.decode(valuePair[0], "UTF-8");
results.put(name, value);
}
} catch (UnsupportedEncodingException e) {
// This should never happen.
Log.e(TAG, "UTF-8 Not Recognized as a charset. Device configuration Error.");
}
}
}

View File

@@ -0,0 +1,81 @@
/*************************************************************************/
/* Dictionary.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot;
import java.util.HashMap;
import java.util.Set;
public class Dictionary extends HashMap<String, Object> {
protected String[] keys_cache;
public String[] get_keys() {
String[] ret = new String[size()];
int i = 0;
Set<String> keys = keySet();
for (String key : keys) {
ret[i] = key;
i++;
};
return ret;
};
public Object[] get_values() {
Object[] ret = new Object[size()];
int i = 0;
Set<String> keys = keySet();
for (String key : keys) {
ret[i] = get(key);
i++;
};
return ret;
};
public void set_keys(String[] keys) {
keys_cache = keys;
};
public void set_values(Object[] vals) {
int i = 0;
for (String key : keys_cache) {
put(key, vals[i]);
i++;
};
keys_cache = null;
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
/*************************************************************************/
/* GodotDownloaderAlarmReceiver.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;
import com.google.android.vending.expansion.downloader.DownloaderClientMarshaller;
/**
* You should start your derived downloader class when this receiver gets the message
* from the alarm service using the provided service helper function within the
* DownloaderClientMarshaller. This class must be then registered in your AndroidManifest.xml
* file with a section like this:
* <receiver android:name=".GodotDownloaderAlarmReceiver"/>
*/
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

@@ -0,0 +1,84 @@
/*************************************************************************/
/* GodotDownloaderService.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.google.android.vending.expansion.downloader.impl.DownloaderService;
/**
* This class demonstrates the minimal client implementation of the
* DownloaderService from the Downloader library.
*/
public class GodotDownloaderService extends DownloaderService {
// stuff for LVL -- MODIFY FOR YOUR APPLICATION!
private static final String BASE64_PUBLIC_KEY = "REPLACE THIS WITH YOUR PUBLIC KEY";
// used by the preference obfuscater
private static final byte[] SALT = new byte[] {
1, 43, -12, -1, 54, 98,
-100, -12, 43, 2, -8, -4, 9, 5, -106, -108, -33, 45, -1, 84
};
/**
* This public key comes from your Android Market publisher account, and it
* used by the LVL to validate responses from Market on your behalf.
*/
@Override
public String getPublicKey() {
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;
}
/**
* This is used by the preference obfuscater to make sure that your
* obfuscated preferences are different than the ones used by other
* applications.
*/
@Override
public byte[] getSALT() {
return SALT;
}
/**
* Fill this in with the class name for your alarm receiver. We do this
* because receivers must be unique across all of Android (it's a good idea
* to make sure that your receiver is in your unique package)
*/
@Override
public String getAlarmReceiverClassName() {
Log.d("GODOT", "getAlarmReceiverClassName()");
return GodotDownloaderAlarmReceiver.class.getName();
}
}

View File

@@ -0,0 +1,631 @@
/*************************************************************************/
/* GodotIO.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot;
import android.content.*;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.AssetManager;
import android.media.*;
import android.net.Uri;
import android.os.*;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import org.godotengine.godot.input.*;
//android.os.Build
// Wrapper for native library
public class GodotIO {
AssetManager am;
Godot activity;
GodotEditText edit;
MediaPlayer mediaPlayer;
final int SCREEN_LANDSCAPE = 0;
final int SCREEN_PORTRAIT = 1;
final int SCREEN_REVERSE_LANDSCAPE = 2;
final int SCREEN_REVERSE_PORTRAIT = 3;
final int SCREEN_SENSOR_LANDSCAPE = 4;
final int SCREEN_SENSOR_PORTRAIT = 5;
final int SCREEN_SENSOR = 6;
/////////////////////////
/// FILES
/////////////////////////
public int last_file_id = 1;
class AssetData {
public boolean eof = false;
public String path;
public InputStream is;
public int len;
public int pos;
}
SparseArray<AssetData> streams;
public int file_open(String path, boolean write) {
//System.out.printf("file_open: Attempt to Open %s\n",path);
//Log.v("MyApp", "TRYING TO OPEN FILE: " + path);
if (write)
return -1;
AssetData ad = new AssetData();
try {
ad.is = am.open(path);
} catch (Exception e) {
//System.out.printf("Exception on file_open: %s\n",path);
return -1;
}
try {
ad.len = ad.is.available();
} catch (Exception e) {
System.out.printf("Exception availabling on file_open: %s\n", path);
return -1;
}
ad.path = path;
ad.pos = 0;
++last_file_id;
streams.put(last_file_id, ad);
return last_file_id;
}
public int file_get_size(int id) {
if (streams.get(id) == null) {
System.out.printf("file_get_size: Invalid file id: %d\n", id);
return -1;
}
return streams.get(id).len;
}
public void file_seek(int id, int bytes) {
if (streams.get(id) == null) {
System.out.printf("file_get_size: Invalid file id: %d\n", id);
return;
}
//seek sucks
AssetData ad = streams.get(id);
if (bytes > ad.len)
bytes = ad.len;
if (bytes < 0)
bytes = 0;
try {
if (bytes > (int)ad.pos) {
int todo = bytes - (int)ad.pos;
while (todo > 0) {
todo -= ad.is.skip(todo);
}
ad.pos = bytes;
} else if (bytes < (int)ad.pos) {
ad.is = am.open(ad.path);
ad.pos = bytes;
int todo = bytes;
while (todo > 0) {
todo -= ad.is.skip(todo);
}
}
ad.eof = false;
} catch (IOException e) {
System.out.printf("Exception on file_seek: %s\n", e);
return;
}
}
public int file_tell(int id) {
if (streams.get(id) == null) {
System.out.printf("file_read: Can't tell eof for invalid file id: %d\n", id);
return 0;
}
AssetData ad = streams.get(id);
return ad.pos;
}
public boolean file_eof(int id) {
if (streams.get(id) == null) {
System.out.printf("file_read: Can't check eof for invalid file id: %d\n", id);
return false;
}
AssetData ad = streams.get(id);
return ad.eof;
}
public byte[] file_read(int id, int bytes) {
if (streams.get(id) == null) {
System.out.printf("file_read: Can't read invalid file id: %d\n", id);
return new byte[0];
}
AssetData ad = streams.get(id);
if (ad.pos + bytes > ad.len) {
bytes = ad.len - ad.pos;
ad.eof = true;
}
if (bytes == 0) {
return new byte[0];
}
byte[] buf1 = new byte[bytes];
int r = 0;
try {
r = ad.is.read(buf1);
} catch (IOException e) {
System.out.printf("Exception on file_read: %s\n", e);
return new byte[bytes];
}
if (r == 0) {
return new byte[0];
}
ad.pos += r;
if (r < bytes) {
byte[] buf2 = new byte[r];
for (int i = 0; i < r; i++)
buf2[i] = buf1[i];
return buf2;
} else {
return buf1;
}
}
public void file_close(int id) {
if (streams.get(id) == null) {
System.out.printf("file_close: Can't close invalid file id: %d\n", id);
return;
}
streams.remove(id);
}
/////////////////////////
/// DIRECTORIES
/////////////////////////
class AssetDir {
public String[] files;
public int current;
public String path;
}
public int last_dir_id = 1;
SparseArray<AssetDir> dirs;
public int dir_open(String path) {
AssetDir ad = new AssetDir();
ad.current = 0;
ad.path = path;
try {
ad.files = am.list(path);
// no way to find path is directory or file exactly.
// but if ad.files.length==0, then it's an empty directory or file.
if (ad.files.length == 0) {
return -1;
}
} catch (IOException e) {
System.out.printf("Exception on dir_open: %s\n", e);
return -1;
}
//System.out.printf("Opened dir: %s\n",path);
++last_dir_id;
dirs.put(last_dir_id, ad);
return last_dir_id;
}
public boolean dir_is_dir(int id) {
if (dirs.get(id) == null) {
System.out.printf("dir_next: invalid dir id: %d\n", id);
return false;
}
AssetDir ad = dirs.get(id);
//System.out.printf("go next: %d,%d\n",ad.current,ad.files.length);
int idx = ad.current;
if (idx > 0)
idx--;
if (idx >= ad.files.length)
return false;
String fname = ad.files[idx];
try {
if (ad.path.equals(""))
am.open(fname);
else
am.open(ad.path + "/" + fname);
return false;
} catch (Exception e) {
return true;
}
}
public String dir_next(int id) {
if (dirs.get(id) == null) {
System.out.printf("dir_next: invalid dir id: %d\n", id);
return "";
}
AssetDir ad = dirs.get(id);
//System.out.printf("go next: %d,%d\n",ad.current,ad.files.length);
if (ad.current >= ad.files.length) {
ad.current++;
return "";
}
String r = ad.files[ad.current];
ad.current++;
return r;
}
public void dir_close(int id) {
if (dirs.get(id) == null) {
System.out.printf("dir_close: invalid dir id: %d\n", id);
return;
}
dirs.remove(id);
}
GodotIO(Godot p_activity) {
am = p_activity.getAssets();
activity = p_activity;
//streams = new HashMap<Integer, AssetData>();
streams = new SparseArray<AssetData>();
dirs = new SparseArray<AssetDir>();
}
/////////////////////////
// AUDIO
/////////////////////////
private Object buf;
private Thread mAudioThread;
private AudioTrack mAudioTrack;
public Object audioInit(int sampleRate, int desiredFrames) {
int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int frameSize = 4;
System.out.printf("audioInit: initializing audio:\n");
//Log.v("Godot", "Godot audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + ((float)sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer");
// Let the user pick a larger buffer if they really want -- but ye
// gods they probably shouldn't, the minimums are horrifyingly high
// latency already
desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize);
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
audioStartThread();
//Log.v("Godot", "Godot audio: got " + ((mAudioTrack.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioTrack.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + ((float)mAudioTrack.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer");
buf = new short[desiredFrames * 2];
return buf;
}
public void audioStartThread() {
mAudioThread = new Thread(new Runnable() {
public void run() {
mAudioTrack.play();
GodotLib.audio();
}
});
// I'd take REALTIME if I could get it!
mAudioThread.setPriority(Thread.MAX_PRIORITY);
mAudioThread.start();
}
public void audioWriteShortBuffer(short[] buffer) {
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// Nom nom
}
} else {
Log.w("Godot", "Godot audio: error return from write(short)");
return;
}
}
}
public void audioQuit() {
if (mAudioThread != null) {
try {
mAudioThread.join();
} catch (Exception e) {
Log.v("Godot", "Problem stopping audio thread: " + e);
}
mAudioThread = null;
//Log.v("Godot", "Finished waiting for audio thread");
}
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack = null;
}
}
public void audioPause(boolean p_pause) {
if (p_pause)
mAudioTrack.pause();
else
mAudioTrack.play();
}
/////////////////////////
// MISCELLANEOUS OS IO
/////////////////////////
public int openURI(String p_uri) {
try {
Log.v("MyApp", "TRYING TO OPEN URI: " + p_uri);
String path = p_uri;
String type = "";
if (path.startsWith("/")) {
//absolute path to filesystem, prepend file://
path = "file://" + path;
if (p_uri.endsWith(".png") || p_uri.endsWith(".jpg") || p_uri.endsWith(".gif") || p_uri.endsWith(".webp")) {
type = "image/*";
}
}
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
if (!type.equals("")) {
intent.setDataAndType(Uri.parse(path), type);
} else {
intent.setData(Uri.parse(path));
}
activity.startActivity(intent);
return 0;
} catch (ActivityNotFoundException e) {
return 1;
}
}
public String getDataDir() {
return activity.getFilesDir().getAbsolutePath();
}
public String getLocale() {
return Locale.getDefault().toString();
}
public String getModel() {
return Build.MODEL;
}
public int getScreenDPI() {
DisplayMetrics metrics = activity.getApplicationContext().getResources().getDisplayMetrics();
return (int)(metrics.density * 160f);
}
public void showKeyboard(String p_existing_text) {
if (edit != null)
edit.showKeyboard(p_existing_text);
//InputMethodManager inputMgr = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
//inputMgr.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
};
public void hideKeyboard() {
if (edit != null)
edit.hideKeyboard();
};
public void setScreenOrientation(int p_orientation) {
switch (p_orientation) {
case SCREEN_LANDSCAPE: {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} break;
case SCREEN_PORTRAIT: {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} break;
case SCREEN_REVERSE_LANDSCAPE: {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
} break;
case SCREEN_REVERSE_PORTRAIT: {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
} break;
case SCREEN_SENSOR_LANDSCAPE: {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
} break;
case SCREEN_SENSOR_PORTRAIT: {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
} break;
case SCREEN_SENSOR: {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
} break;
}
};
public void setEdit(GodotEditText _edit) {
edit = _edit;
}
public void playVideo(String p_path) {
Uri filePath = Uri.parse(p_path);
mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(activity.getApplicationContext(), filePath);
mediaPlayer.prepare();
mediaPlayer.start();
} catch (IOException e) {
System.out.println("IOError while playing video");
}
}
public boolean isVideoPlaying() {
if (mediaPlayer != null) {
return mediaPlayer.isPlaying();
}
return false;
}
public void pauseVideo() {
if (mediaPlayer != null) {
mediaPlayer.pause();
}
}
public void stopVideo() {
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
}
}
public static final int SYSTEM_DIR_DESKTOP = 0;
public static final int SYSTEM_DIR_DCIM = 1;
public static final int SYSTEM_DIR_DOCUMENTS = 2;
public static final int SYSTEM_DIR_DOWNLOADS = 3;
public static final int SYSTEM_DIR_MOVIES = 4;
public static final int SYSTEM_DIR_MUSIC = 5;
public static final int SYSTEM_DIR_PICTURES = 6;
public static final int SYSTEM_DIR_RINGTONES = 7;
public String getSystemDir(int idx) {
String what = "";
switch (idx) {
case SYSTEM_DIR_DESKTOP: {
//what=Environment.DIRECTORY_DOCUMENTS;
what = Environment.DIRECTORY_DOWNLOADS;
} break;
case SYSTEM_DIR_DCIM: {
what = Environment.DIRECTORY_DCIM;
} break;
case SYSTEM_DIR_DOCUMENTS: {
what = Environment.DIRECTORY_DOWNLOADS;
//what=Environment.DIRECTORY_DOCUMENTS;
} break;
case SYSTEM_DIR_DOWNLOADS: {
what = Environment.DIRECTORY_DOWNLOADS;
} break;
case SYSTEM_DIR_MOVIES: {
what = Environment.DIRECTORY_MOVIES;
} break;
case SYSTEM_DIR_MUSIC: {
what = Environment.DIRECTORY_MUSIC;
} break;
case SYSTEM_DIR_PICTURES: {
what = Environment.DIRECTORY_PICTURES;
} break;
case SYSTEM_DIR_RINGTONES: {
what = Environment.DIRECTORY_RINGTONES;
} break;
}
if (what.equals(""))
return "";
return Environment.getExternalStoragePublicDirectory(what).getAbsolutePath();
}
protected static final String PREFS_FILE = "device_id.xml";
protected static final String PREFS_DEVICE_ID = "device_id";
public static String unique_id = "";
public String getUniqueID() {
return unique_id;
}
}

View File

@@ -0,0 +1,50 @@
/*************************************************************************/
/* GodotInstrumentation.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot;
import android.app.Instrumentation;
import android.content.Intent;
import android.os.Bundle;
public class GodotInstrumentation extends Instrumentation {
private Intent intent;
@Override
public void onCreate(Bundle arguments) {
intent = arguments.getParcelable("intent");
start();
}
@Override
public void onStart() {
startActivitySync(intent);
}
}

View File

@@ -0,0 +1,214 @@
/*************************************************************************/
/* GodotLib.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot;
import android.app.Activity;
import android.hardware.SensorEvent;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/**
* Wrapper for native library
*/
public class GodotLib {
public static GodotIO io;
static {
System.loadLibrary("godot_android");
}
/**
* Invoked on the main thread to initialize Godot native layer.
*/
public static native void initialize(Godot p_instance, Object p_asset_manager, boolean use_apk_expansion);
/**
* Invoked on the main thread to clean up Godot native layer.
* @see Activity#onDestroy()
*/
public static native void ondestroy(Godot p_instance);
/**
* Invoked on the GL thread to complete setup for the Godot native layer logic.
* @param p_cmdline Command line arguments used to configure Godot native layer components.
*/
public static native void setup(String[] p_cmdline);
/**
* Invoked on the GL thread when the underlying Android surface has changed size.
* @param width
* @param height
* @see android.opengl.GLSurfaceView.Renderer#onSurfaceChanged(GL10, int, int)
*/
public static native void resize(int width, int height);
/**
* Invoked on the GL thread when the underlying Android surface is created or recreated.
* @param p_32_bits
* @see android.opengl.GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig)
*/
public static native void newcontext(boolean p_32_bits);
/**
* Forward {@link Activity#onBackPressed()} event from the main thread to the GL thread.
*/
public static native void back();
/**
* Invoked on the GL thread to draw the current frame.
* @see android.opengl.GLSurfaceView.Renderer#onDrawFrame(GL10)
*/
public static native void step();
/**
* Forward touch events from the main thread to the GL thread.
*/
public static native void touch(int what, int pointer, int howmany, int[] arr);
/**
* Forward accelerometer sensor events from the main thread to the GL thread.
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
*/
public static native void accelerometer(float x, float y, float z);
/**
* Forward gravity sensor events from the main thread to the GL thread.
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
*/
public static native void gravity(float x, float y, float z);
/**
* Forward magnetometer sensor events from the main thread to the GL thread.
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
*/
public static native void magnetometer(float x, float y, float z);
/**
* Forward gyroscope sensor events from the main thread to the GL thread.
* @see android.hardware.SensorEventListener#onSensorChanged(SensorEvent)
*/
public static native void gyroscope(float x, float y, float z);
/**
* Forward regular key events from the main thread to the GL thread.
*/
public static native void key(int p_scancode, int p_unicode_char, boolean p_pressed);
/**
* Forward game device's key events from the main thread to the GL thread.
*/
public static native void joybutton(int p_device, int p_but, boolean p_pressed);
/**
* Forward joystick devices axis motion events from the main thread to the GL thread.
*/
public static native void joyaxis(int p_device, int p_axis, float p_value);
/**
* Forward joystick devices hat motion events from the main thread to the GL thread.
*/
public static native void joyhat(int p_device, int p_hat_x, int p_hat_y);
/**
* Fires when a joystick device is added or removed.
*/
public static native void joyconnectionchanged(int p_device, boolean p_connected, String p_name);
/**
* Invoked when the Android activity resumes.
* @see Activity#onResume()
*/
public static native void focusin();
/**
* Invoked when the Android activity pauses.
* @see Activity#onPause()
*/
public static native void focusout();
/**
* Invoked when the audio thread is started.
*/
public static native void audio();
/**
* Used to setup a {@link org.godotengine.godot.Godot.SingletonBase} instance.
* @param p_name Name of the instance.
* @param p_object Reference to the singleton instance.
*/
public static native void singleton(String p_name, Object p_object);
/**
* Used to complete registration of the {@link org.godotengine.godot.Godot.SingletonBase} instance's methods.
* @param p_sname Name of the instance
* @param p_name Name of the method to register
* @param p_ret Return type of the registered method
* @param p_params Method parameters types
*/
public static native void method(String p_sname, String p_name, String p_ret, String[] p_params);
/**
* Used to access Godot global properties.
* @param p_key Property key
* @return String value of the property
*/
public static native String getGlobal(String p_key);
/**
* Invoke method |p_method| on the Godot object specified by |p_id|
* @param p_id Id of the Godot object to invoke
* @param p_method Name of the method to invoke
* @param p_params Parameters to use for method invocation
*/
public static native void callobject(int p_id, String p_method, Object[] p_params);
/**
* Invoke method |p_method| on the Godot object specified by |p_id| during idle time.
* @param p_id Id of the Godot object to invoke
* @param p_method Name of the method to invoke
* @param p_params Parameters to use for method invocation
*/
public static native void calldeferred(int p_id, String p_method, Object[] p_params);
/**
* Forward the results from a permission request.
* @see Activity#onRequestPermissionsResult(int, String[], int[])
* @param p_permission Request permission
* @param p_result True if the permission was granted, false otherwise
*/
public static native void requestPermissionResult(String p_permission, boolean p_result);
/**
* Invoked on the GL thread to configure the height of the virtual keyboard.
*/
public static native void setVirtualKeyboardHeight(int p_height);
}

View File

@@ -0,0 +1,230 @@
/*************************************************************************/
/* GodotPaymentV3.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot;
import android.app.Activity;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.godotengine.godot.payments.PaymentsManager;
import org.json.JSONException;
import org.json.JSONObject;
public class GodotPaymentV3 extends Godot.SingletonBase {
private Godot activity;
private Integer purchaseCallbackId = 0;
private String accessToken;
private String purchaseValidationUrlPrefix;
private String transactionId;
private PaymentsManager mPaymentManager;
private Dictionary mSkuDetails = new Dictionary();
public void purchase(final String sku, final String transactionId) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
mPaymentManager.requestPurchase(sku, transactionId);
}
});
}
static public Godot.SingletonBase initialize(Activity p_activity) {
return new GodotPaymentV3(p_activity);
}
public GodotPaymentV3(Activity p_activity) {
registerClass("GodotPayments", new String[] { "purchase", "setPurchaseCallbackId", "setPurchaseValidationUrlPrefix", "setTransactionId", "getSignature", "consumeUnconsumedPurchases", "requestPurchased", "setAutoConsume", "consume", "querySkuDetails", "isConnected" });
activity = (Godot)p_activity;
mPaymentManager = activity.getPaymentsManager();
mPaymentManager.setBaseSingleton(this);
}
public void consumeUnconsumedPurchases() {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
mPaymentManager.consumeUnconsumedPurchases();
}
});
}
private String signature;
public String getSignature() {
return this.signature;
}
public void callbackSuccess(String ticket, String signature, String sku) {
GodotLib.calldeferred(purchaseCallbackId, "purchase_success", new Object[] { ticket, signature, sku });
}
public void callbackSuccessProductMassConsumed(String ticket, String signature, String sku) {
Log.d(this.getClass().getName(), "callbackSuccessProductMassConsumed > " + ticket + "," + signature + "," + sku);
GodotLib.calldeferred(purchaseCallbackId, "consume_success", new Object[] { ticket, signature, sku });
}
public void callbackSuccessNoUnconsumedPurchases() {
GodotLib.calldeferred(purchaseCallbackId, "consume_not_required", new Object[] {});
}
public void callbackFailConsume(String message) {
GodotLib.calldeferred(purchaseCallbackId, "consume_fail", new Object[] { message });
}
public void callbackFail(String message) {
GodotLib.calldeferred(purchaseCallbackId, "purchase_fail", new Object[] { message });
}
public void callbackCancel() {
GodotLib.calldeferred(purchaseCallbackId, "purchase_cancel", new Object[] {});
}
public void callbackAlreadyOwned(String sku) {
GodotLib.calldeferred(purchaseCallbackId, "purchase_owned", new Object[] { sku });
}
public int getPurchaseCallbackId() {
return purchaseCallbackId;
}
public void setPurchaseCallbackId(int purchaseCallbackId) {
this.purchaseCallbackId = purchaseCallbackId;
}
public String getPurchaseValidationUrlPrefix() {
return this.purchaseValidationUrlPrefix;
}
public void setPurchaseValidationUrlPrefix(String url) {
this.purchaseValidationUrlPrefix = url;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}
public String getTransactionId() {
return this.transactionId;
}
// request purchased items are not consumed
public void requestPurchased() {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
mPaymentManager.requestPurchased();
}
});
}
// callback for requestPurchased()
public void callbackPurchased(String receipt, String signature, String sku) {
GodotLib.calldeferred(purchaseCallbackId, "has_purchased", new Object[] { receipt, signature, sku });
}
public void callbackDisconnected() {
GodotLib.calldeferred(purchaseCallbackId, "iap_disconnected", new Object[] {});
}
public void callbackConnected() {
GodotLib.calldeferred(purchaseCallbackId, "iap_connected", new Object[] {});
}
// true if connected, false otherwise
public boolean isConnected() {
return mPaymentManager.isConnected();
}
// consume item automatically after purchase. default is true.
public void setAutoConsume(boolean autoConsume) {
mPaymentManager.setAutoConsume(autoConsume);
}
// consume a specific item
public void consume(String sku) {
mPaymentManager.consume(sku);
}
// query in app item detail info
public void querySkuDetails(String[] list) {
List<String> nKeys = Arrays.asList(list);
List<String> cKeys = Arrays.asList(mSkuDetails.get_keys());
ArrayList<String> fKeys = new ArrayList<String>();
for (String key : nKeys) {
if (!cKeys.contains(key)) {
fKeys.add(key);
}
}
if (fKeys.size() > 0) {
mPaymentManager.querySkuDetails(fKeys.toArray(new String[0]));
} else {
completeSkuDetail();
}
}
public void addSkuDetail(String itemJson) {
JSONObject o = null;
try {
o = new JSONObject(itemJson);
Dictionary item = new Dictionary();
item.put("type", o.optString("type"));
item.put("product_id", o.optString("productId"));
item.put("title", o.optString("title"));
item.put("description", o.optString("description"));
item.put("price", o.optString("price"));
item.put("price_currency_code", o.optString("price_currency_code"));
item.put("price_amount", 0.000001d * o.optLong("price_amount_micros"));
mSkuDetails.put(item.get("product_id").toString(), item);
} catch (JSONException e) {
e.printStackTrace();
}
}
public void completeSkuDetail() {
GodotLib.calldeferred(purchaseCallbackId, "sku_details_complete", new Object[] { mSkuDetails });
}
public void errorSkuDetail(String errorMessage) {
GodotLib.calldeferred(purchaseCallbackId, "sku_details_error", new Object[] { errorMessage });
}
}

View File

@@ -0,0 +1,61 @@
/*************************************************************************/
/* GodotRenderer.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot;
import android.opengl.GLSurfaceView;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import org.godotengine.godot.utils.GLUtils;
/**
* Godot's renderer implementation.
*/
class GodotRenderer implements GLSurfaceView.Renderer {
public void onDrawFrame(GL10 gl) {
GodotLib.step();
for (int i = 0; i < Godot.singleton_count; i++) {
Godot.singletons[i].onGLDrawFrame(gl);
}
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
GodotLib.resize(width, height);
for (int i = 0; i < Godot.singleton_count; i++) {
Godot.singletons[i].onGLSurfaceChanged(gl, width, height);
}
}
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GodotLib.newcontext(GLUtils.use_32);
}
}

View File

@@ -0,0 +1,170 @@
/*************************************************************************/
/* GodotView.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot;
import android.annotation.SuppressLint;
import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView;
import android.view.KeyEvent;
import android.view.MotionEvent;
import org.godotengine.godot.input.GodotInputHandler;
import org.godotengine.godot.utils.GLUtils;
import org.godotengine.godot.xr.XRMode;
import org.godotengine.godot.xr.ovr.OvrConfigChooser;
import org.godotengine.godot.xr.ovr.OvrContextFactory;
import org.godotengine.godot.xr.ovr.OvrWindowSurfaceFactory;
import org.godotengine.godot.xr.regular.RegularConfigChooser;
import org.godotengine.godot.xr.regular.RegularContextFactory;
import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser;
/**
* A simple GLSurfaceView sub-class that demonstrate how to perform
* OpenGL ES 2.0 rendering into a GL Surface. Note the following important
* details:
*
* - The class must use a custom context factory to enable 2.0 rendering.
* See ContextFactory class definition below.
*
* - The class must use a custom EGLConfigChooser to be able to select
* an EGLConfig that supports 2.0. This is done by providing a config
* specification to eglChooseConfig() that has the attribute
* EGL10.ELG_RENDERABLE_TYPE containing the EGL_OPENGL_ES2_BIT flag
* set. See ConfigChooser class definition below.
*
* - The class must select the surface's format, then choose an EGLConfig
* that matches it exactly (with regards to red/green/blue/alpha channels
* bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
*/
public class GodotView extends GLSurfaceView {
private static String TAG = GodotView.class.getSimpleName();
private final Godot activity;
private final GodotInputHandler inputHandler;
public GodotView(Godot activity, XRMode xrMode, boolean p_use_gl3, boolean p_use_32_bits, boolean p_use_debug_opengl) {
super(activity);
GLUtils.use_gl3 = p_use_gl3;
GLUtils.use_32 = p_use_32_bits;
GLUtils.use_debug_opengl = p_use_debug_opengl;
this.activity = activity;
this.inputHandler = new GodotInputHandler(this);
init(xrMode, false, 16, 0);
}
public void initInputDevices() {
this.inputHandler.initInputDevices();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
return activity.gotTouchEvent(event);
}
@Override
public boolean onKeyUp(final int keyCode, KeyEvent event) {
return inputHandler.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyDown(final int keyCode, KeyEvent event) {
return inputHandler.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
return inputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event);
}
private void init(XRMode xrMode, boolean translucent, int depth, int stencil) {
setPreserveEGLContextOnPause(true);
setFocusableInTouchMode(true);
switch (xrMode) {
case OVR:
// Replace the default egl config chooser.
setEGLConfigChooser(new OvrConfigChooser());
// Replace the default context factory.
setEGLContextFactory(new OvrContextFactory());
// Replace the default window surface factory.
setEGLWindowSurfaceFactory(new OvrWindowSurfaceFactory());
break;
case REGULAR:
default:
/* By default, GLSurfaceView() creates a RGB_565 opaque surface.
* If we want a translucent one, we should change the surface's
* format here, using PixelFormat.TRANSLUCENT for GL Surfaces
* is interpreted as any 32-bit surface with alpha by SurfaceFlinger.
*/
if (translucent) {
this.getHolder().setFormat(PixelFormat.TRANSLUCENT);
}
/* Setup the context factory for 2.0 rendering.
* See ContextFactory class definition below
*/
setEGLContextFactory(new RegularContextFactory());
/* We need to choose an EGLConfig that matches the format of
* our surface exactly. This is going to be done in our
* custom config chooser. See ConfigChooser class definition
* below.
*/
if (GLUtils.use_32) {
setEGLConfigChooser(translucent ?
new RegularFallbackConfigChooser(8, 8, 8, 8, 24, stencil,
new RegularConfigChooser(8, 8, 8, 8, 16, stencil)) :
new RegularFallbackConfigChooser(8, 8, 8, 8, 24, stencil,
new RegularConfigChooser(5, 6, 5, 0, 16, stencil)));
} else {
setEGLConfigChooser(translucent ?
new RegularConfigChooser(8, 8, 8, 8, 16, stencil) :
new RegularConfigChooser(5, 6, 5, 0, 16, stencil));
}
break;
}
/* Set the renderer responsible for frame rendering */
setRenderer(new GodotRenderer());
}
public void onBackPressed() {
activity.onBackPressed();
}
}

View File

@@ -0,0 +1,171 @@
/*************************************************************************/
/* GodotEditText.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.input;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import java.lang.ref.WeakReference;
import org.godotengine.godot.*;
public class GodotEditText extends EditText {
// ===========================================================
// Constants
// ===========================================================
private final static int HANDLER_OPEN_IME_KEYBOARD = 2;
private final static int HANDLER_CLOSE_IME_KEYBOARD = 3;
// ===========================================================
// Fields
// ===========================================================
private GodotView mView;
private GodotTextInputWrapper mInputWrapper;
private EditHandler sHandler = new EditHandler(this);
private String mOriginText;
private static class EditHandler extends Handler {
private final WeakReference<GodotEditText> mEdit;
public EditHandler(GodotEditText edit) {
mEdit = new WeakReference<>(edit);
}
@Override
public void handleMessage(Message msg) {
GodotEditText edit = mEdit.get();
if (edit != null) {
edit.handleMessage(msg);
}
}
}
// ===========================================================
// Constructors
// ===========================================================
public GodotEditText(final Context context) {
super(context);
this.initView();
}
public GodotEditText(final Context context, final AttributeSet attrs) {
super(context, attrs);
this.initView();
}
public GodotEditText(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
this.initView();
}
protected void initView() {
this.setPadding(0, 0, 0, 0);
this.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
}
private void handleMessage(final Message msg) {
switch (msg.what) {
case HANDLER_OPEN_IME_KEYBOARD: {
GodotEditText edit = (GodotEditText)msg.obj;
String text = edit.mOriginText;
if (edit.requestFocus()) {
edit.removeTextChangedListener(edit.mInputWrapper);
edit.setText("");
edit.append(text);
edit.mInputWrapper.setOriginText(text);
edit.addTextChangedListener(edit.mInputWrapper);
final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(edit, 0);
}
} break;
case HANDLER_CLOSE_IME_KEYBOARD: {
GodotEditText edit = (GodotEditText)msg.obj;
edit.removeTextChangedListener(mInputWrapper);
final InputMethodManager imm = (InputMethodManager)mView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(edit.getWindowToken(), 0);
edit.mView.requestFocus();
} break;
}
}
// ===========================================================
// Getter & Setter
// ===========================================================
public void setView(final GodotView view) {
this.mView = view;
if (mInputWrapper == null)
mInputWrapper = new GodotTextInputWrapper(mView, this);
this.setOnEditorActionListener(mInputWrapper);
view.requestFocus();
}
// ===========================================================
// Methods for/from SuperClass/Interfaces
// ===========================================================
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
super.onKeyDown(keyCode, keyEvent);
/* Let GlSurfaceView get focus if back key is input. */
if (keyCode == KeyEvent.KEYCODE_BACK) {
this.mView.requestFocus();
}
return true;
}
// ===========================================================
// Methods
// ===========================================================
public void showKeyboard(String p_existing_text) {
this.mOriginText = p_existing_text;
final Message msg = new Message();
msg.what = HANDLER_OPEN_IME_KEYBOARD;
msg.obj = this;
sHandler.sendMessage(msg);
}
public void hideKeyboard() {
final Message msg = new Message();
msg.what = HANDLER_CLOSE_IME_KEYBOARD;
msg.obj = this;
sHandler.sendMessage(msg);
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
}

View File

@@ -0,0 +1,360 @@
/*************************************************************************/
/* GodotInputHandler.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.input;
import static org.godotengine.godot.utils.GLUtils.DEBUG;
import android.util.Log;
import android.view.InputDevice;
import android.view.InputDevice.MotionRange;
import android.view.KeyEvent;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.godotengine.godot.GodotLib;
import org.godotengine.godot.GodotView;
import org.godotengine.godot.input.InputManagerCompat.InputDeviceListener;
/**
* Handles input related events for the {@link GodotView} view.
*/
public class GodotInputHandler implements InputDeviceListener {
private final ArrayList<Joystick> joysticksDevices = new ArrayList<Joystick>();
private final GodotView godotView;
private final InputManagerCompat inputManager;
public GodotInputHandler(GodotView godotView) {
this.godotView = godotView;
this.inputManager = InputManagerCompat.Factory.getInputManager(godotView.getContext());
this.inputManager.registerInputDeviceListener(this, null);
}
private void queueEvent(Runnable task) {
godotView.queueEvent(task);
}
private boolean isKeyEvent_GameDevice(int source) {
// Note that keyboards are often (SOURCE_KEYBOARD | SOURCE_DPAD)
if (source == (InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD))
return false;
return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
}
public boolean onKeyUp(final int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
return true;
}
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
return false;
};
int source = event.getSource();
if (isKeyEvent_GameDevice(source)) {
final int button = getGodotButton(keyCode);
final int device_id = findJoystickDevice(event.getDeviceId());
// Check if the device exists
if (device_id > -1) {
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joybutton(device_id, button, false);
}
});
return true;
}
} else {
final int chr = event.getUnicodeChar(0);
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.key(keyCode, chr, false);
}
});
};
return false;
}
public boolean onKeyDown(final int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
godotView.onBackPressed();
// press 'back' button should not terminate program
//normal handle 'back' event in game logic
return true;
}
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
return false;
};
int source = event.getSource();
//Log.e(TAG, String.format("Key down! source %d, device %d, joystick %d, %d, %d", event.getDeviceId(), source, (source & InputDevice.SOURCE_JOYSTICK), (source & InputDevice.SOURCE_DPAD), (source & InputDevice.SOURCE_GAMEPAD)));
if (isKeyEvent_GameDevice(source)) {
if (event.getRepeatCount() > 0) // ignore key echo
return true;
final int button = getGodotButton(keyCode);
final int device_id = findJoystickDevice(event.getDeviceId());
// Check if the device exists
if (device_id > -1) {
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joybutton(device_id, button, true);
}
});
return true;
}
} else {
final int chr = event.getUnicodeChar(0);
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.key(keyCode, chr, true);
}
});
};
return false;
}
public boolean onGenericMotionEvent(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.getAction() == MotionEvent.ACTION_MOVE) {
final int device_id = findJoystickDevice(event.getDeviceId());
// Check if the device exists
if (device_id > -1) {
Joystick joy = joysticksDevices.get(device_id);
for (int i = 0; i < joy.axes.size(); i++) {
InputDevice.MotionRange range = joy.axes.get(i);
final float value = (event.getAxisValue(range.getAxis()) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
final int idx = i;
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyaxis(device_id, idx, value);
}
});
}
for (int i = 0; i < joy.hats.size(); i += 2) {
final int hatX = Math.round(event.getAxisValue(joy.hats.get(i).getAxis()));
final int hatY = Math.round(event.getAxisValue(joy.hats.get(i + 1).getAxis()));
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyhat(device_id, hatX, hatY);
}
});
}
return true;
}
};
return false;
}
public void initInputDevices() {
/* initially add input devices*/
int[] deviceIds = inputManager.getInputDeviceIds();
for (int deviceId : deviceIds) {
InputDevice device = inputManager.getInputDevice(deviceId);
if (DEBUG) {
Log.v("GodotView", String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName()));
}
onInputDeviceAdded(deviceId);
}
}
@Override
public void onInputDeviceAdded(int deviceId) {
int id = findJoystickDevice(deviceId);
// Check if the device has not been already added
if (id < 0) {
InputDevice device = inputManager.getInputDevice(deviceId);
//device can be null if deviceId is not found
if (device != null) {
int sources = device.getSources();
if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) {
id = joysticksDevices.size();
Joystick joy = new Joystick();
joy.device_id = deviceId;
joy.name = device.getName();
joy.axes = new ArrayList<InputDevice.MotionRange>();
joy.hats = new ArrayList<InputDevice.MotionRange>();
List<InputDevice.MotionRange> ranges = device.getMotionRanges();
Collections.sort(ranges, new RangeComparator());
for (InputDevice.MotionRange range : ranges) {
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
joy.hats.add(range);
} else {
joy.axes.add(range);
}
}
joysticksDevices.add(joy);
final int device_id = id;
final String name = joy.name;
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyconnectionchanged(device_id, true, name);
}
});
}
}
}
}
@Override
public void onInputDeviceRemoved(int deviceId) {
final int device_id = findJoystickDevice(deviceId);
// Check if the evice has not been already removed
if (device_id > -1) {
joysticksDevices.remove(device_id);
queueEvent(new Runnable() {
@Override
public void run() {
GodotLib.joyconnectionchanged(device_id, false, "");
}
});
}
}
@Override
public void onInputDeviceChanged(int deviceId) {
onInputDeviceRemoved(deviceId);
onInputDeviceAdded(deviceId);
}
private static class RangeComparator implements Comparator<MotionRange> {
@Override
public int compare(MotionRange arg0, MotionRange arg1) {
return arg0.getAxis() - arg1.getAxis();
}
}
public static int getGodotButton(int keyCode) {
int button;
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_A: // Android A is SNES B
button = 0;
break;
case KeyEvent.KEYCODE_BUTTON_B:
button = 1;
break;
case KeyEvent.KEYCODE_BUTTON_X: // Android X is SNES Y
button = 2;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
button = 3;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
button = 9;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
button = 15;
break;
case KeyEvent.KEYCODE_BUTTON_R1:
button = 10;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
button = 16;
break;
case KeyEvent.KEYCODE_BUTTON_SELECT:
button = 4;
break;
case KeyEvent.KEYCODE_BUTTON_START:
button = 6;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
button = 7;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
button = 8;
break;
case KeyEvent.KEYCODE_DPAD_UP:
button = 11;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
button = 12;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
button = 13;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
button = 14;
break;
case KeyEvent.KEYCODE_BUTTON_C:
button = 17;
break;
case KeyEvent.KEYCODE_BUTTON_Z:
button = 18;
break;
default:
button = keyCode - KeyEvent.KEYCODE_BUTTON_1 + 20;
break;
}
return button;
}
private int findJoystickDevice(int device_id) {
for (int i = 0; i < joysticksDevices.size(); i++) {
if (joysticksDevices.get(i).device_id == device_id) {
return i;
}
}
return -1;
}
}

View File

@@ -0,0 +1,150 @@
/*************************************************************************/
/* GodotTextInputWrapper.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.input;
import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import org.godotengine.godot.*;
public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListener {
// ===========================================================
// Constants
// ===========================================================
private static final String TAG = GodotTextInputWrapper.class.getSimpleName();
// ===========================================================
// Fields
// ===========================================================
private final GodotView mView;
private final GodotEditText mEdit;
private String mOriginText;
// ===========================================================
// Constructors
// ===========================================================
public GodotTextInputWrapper(final GodotView view, final GodotEditText edit) {
this.mView = view;
this.mEdit = edit;
}
// ===========================================================
// Getter & Setter
// ===========================================================
private boolean isFullScreenEdit() {
final TextView textField = this.mEdit;
final InputMethodManager imm = (InputMethodManager)textField.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
return imm.isFullscreenMode();
}
public void setOriginText(final String originText) {
this.mOriginText = originText;
}
// ===========================================================
// Methods for/from SuperClass/Interfaces
// ===========================================================
@Override
public void afterTextChanged(final Editable s) {
}
@Override
public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) {
//Log.d(TAG, "beforeTextChanged(" + pCharSequence + ")start: " + start + ",count: " + count + ",after: " + after);
mView.queueEvent(new Runnable() {
@Override
public void run() {
for (int i = 0; i < count; ++i) {
GodotLib.key(KeyEvent.KEYCODE_DEL, 0, true);
GodotLib.key(KeyEvent.KEYCODE_DEL, 0, false);
}
}
});
}
@Override
public void onTextChanged(final CharSequence pCharSequence, final int start, final int before, final int count) {
//Log.d(TAG, "onTextChanged(" + pCharSequence + ")start: " + start + ",count: " + count + ",before: " + before);
final int[] newChars = new int[count];
for (int i = start; i < start + count; ++i) {
newChars[i - start] = pCharSequence.charAt(i);
}
mView.queueEvent(new Runnable() {
@Override
public void run() {
for (int i = 0; i < count; ++i) {
GodotLib.key(0, newChars[i], true);
GodotLib.key(0, newChars[i], false);
}
}
});
}
@Override
public boolean onEditorAction(final TextView pTextView, final int pActionID, final KeyEvent pKeyEvent) {
if (this.mEdit == pTextView && this.isFullScreenEdit()) {
final String characters = pKeyEvent.getCharacters();
mView.queueEvent(new Runnable() {
@Override
public void run() {
for (int i = 0; i < characters.length(); i++) {
final int ch = characters.codePointAt(i);
GodotLib.key(0, ch, true);
GodotLib.key(0, ch, false);
}
}
});
}
if (pActionID == EditorInfo.IME_ACTION_DONE) {
this.mView.requestFocus();
}
return false;
}
// ===========================================================
// Methods
// ===========================================================
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (C) 2013 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 org.godotengine.godot.input;
import android.content.Context;
import android.os.Handler;
import android.view.InputDevice;
import android.view.MotionEvent;
public interface InputManagerCompat {
/**
* Gets information about the input device with the specified id.
*
* @param id The device id
* @return The input device or null if not found
*/
public InputDevice getInputDevice(int id);
/**
* Gets the ids of all input devices in the system.
*
* @return The input device ids.
*/
public int[] getInputDeviceIds();
/**
* Registers an input device listener to receive notifications about when
* input devices are added, removed or changed.
*
* @param listener The listener to register.
* @param handler The handler on which the listener should be invoked, or
* null if the listener should be invoked on the calling thread's
* looper.
*/
public void registerInputDeviceListener(InputManagerCompat.InputDeviceListener listener,
Handler handler);
/**
* Unregisters an input device listener.
*
* @param listener The listener to unregister.
*/
public void unregisterInputDeviceListener(InputManagerCompat.InputDeviceListener listener);
/*
* The following three calls are to simulate V16 behavior on pre-Jellybean
* devices. If you don't call them, your callback will never be called
* pre-API 16.
*/
/**
* Pass the motion events to the InputManagerCompat. This is used to
* optimize for polling for controllers. If you do not pass these events in,
* polling will cause regular object creation.
*
* @param event the motion event from the app
*/
public void onGenericMotionEvent(MotionEvent event);
/**
* Tell the V9 input manager that it should stop polling for disconnected
* devices. You can call this during onPause in your activity, although you
* might want to call it whenever your game is not active (or whenever you
* don't care about being notified of new input devices)
*/
public void onPause();
/**
* Tell the V9 input manager that it should start polling for disconnected
* devices. You can call this during onResume in your activity, although you
* might want to call it less often (only when the gameplay is actually
* active)
*/
public void onResume();
public interface InputDeviceListener {
/**
* Called whenever the input manager detects that a device has been
* added. This will only be called in the V9 version when a motion event
* is detected.
*
* @param deviceId The id of the input device that was added.
*/
void onInputDeviceAdded(int deviceId);
/**
* Called whenever the properties of an input device have changed since
* they were last queried. This will not be called for the V9 version of
* the API.
*
* @param deviceId The id of the input device that changed.
*/
void onInputDeviceChanged(int deviceId);
/**
* Called whenever the input manager detects that a device has been
* removed. For the V9 version, this can take some time depending on the
* poll rate.
*
* @param deviceId The id of the input device that was removed.
*/
void onInputDeviceRemoved(int deviceId);
}
/**
* Use this to construct a compatible InputManager.
*/
public static class Factory {
/**
* Constructs and returns a compatible InputManger
*
* @param context the Context that will be used to get the system
* service from
* @return a compatible implementation of InputManager
*/
public static InputManagerCompat getInputManager(Context context) {
return new InputManagerV16(context);
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2013 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 org.godotengine.godot.input;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Build;
import android.os.Handler;
import android.view.InputDevice;
import android.view.MotionEvent;
import java.util.HashMap;
import java.util.Map;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class InputManagerV16 implements InputManagerCompat {
private final InputManager mInputManager;
private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> mListeners;
public InputManagerV16(Context context) {
mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
mListeners = new HashMap<InputManagerCompat.InputDeviceListener, V16InputDeviceListener>();
}
@Override
public InputDevice getInputDevice(int id) {
return mInputManager.getInputDevice(id);
}
@Override
public int[] getInputDeviceIds() {
return mInputManager.getInputDeviceIds();
}
static class V16InputDeviceListener implements InputManager.InputDeviceListener {
final InputManagerCompat.InputDeviceListener mIDL;
public V16InputDeviceListener(InputDeviceListener idl) {
mIDL = idl;
}
@Override
public void onInputDeviceAdded(int deviceId) {
mIDL.onInputDeviceAdded(deviceId);
}
@Override
public void onInputDeviceChanged(int deviceId) {
mIDL.onInputDeviceChanged(deviceId);
}
@Override
public void onInputDeviceRemoved(int deviceId) {
mIDL.onInputDeviceRemoved(deviceId);
}
}
@Override
public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) {
V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener);
mInputManager.registerInputDeviceListener(v16Listener, handler);
mListeners.put(listener, v16Listener);
}
@Override
public void unregisterInputDeviceListener(InputDeviceListener listener) {
V16InputDeviceListener curListener = mListeners.remove(listener);
if (null != curListener) {
mInputManager.unregisterInputDeviceListener(curListener);
}
}
@Override
public void onGenericMotionEvent(MotionEvent event) {
// unused in V16
}
@Override
public void onPause() {
// unused in V16
}
@Override
public void onResume() {
// unused in V16
}
}

View File

@@ -0,0 +1,44 @@
/*************************************************************************/
/* Joystick.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.input;
import android.view.InputDevice.MotionRange;
import java.util.ArrayList;
/**
* POJO class to represent a Joystick input device.
*/
class Joystick {
int device_id;
String name;
ArrayList<MotionRange> axes;
ArrayList<MotionRange> hats;
}

View File

@@ -0,0 +1,116 @@
/*************************************************************************/
/* ConsumeTask.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.payments;
import android.content.Context;
import android.os.AsyncTask;
import android.os.RemoteException;
import com.android.vending.billing.IInAppBillingService;
import java.lang.ref.WeakReference;
abstract public class ConsumeTask {
private Context context;
private IInAppBillingService mService;
private String mSku;
private String mToken;
private static class ConsumeAsyncTask extends AsyncTask<String, String, String> {
private WeakReference<ConsumeTask> mTask;
ConsumeAsyncTask(ConsumeTask consume) {
mTask = new WeakReference<>(consume);
}
@Override
protected String doInBackground(String... strings) {
ConsumeTask consume = mTask.get();
if (consume != null) {
return consume.doInBackground(strings);
}
return null;
}
@Override
protected void onPostExecute(String param) {
ConsumeTask consume = mTask.get();
if (consume != null) {
consume.onPostExecute(param);
}
}
}
public ConsumeTask(IInAppBillingService mService, Context context) {
this.context = context;
this.mService = mService;
}
public void consume(final String sku) {
mSku = sku;
PaymentsCache pc = new PaymentsCache(context);
Boolean isBlocked = pc.getConsumableFlag("block", sku);
mToken = pc.getConsumableValue("token", sku);
if (!isBlocked && mToken == null) {
// Consuming task is processing
} else if (!isBlocked) {
return;
} else if (mToken == null) {
this.error("No token for sku:" + sku);
return;
}
new ConsumeAsyncTask(this).execute();
}
private String doInBackground(String... params) {
try {
int response = mService.consumePurchase(3, context.getPackageName(), mToken);
if (response == 0 || response == 8) {
return null;
}
} catch (RemoteException e) {
return e.getMessage();
}
return "Some error";
}
private void onPostExecute(String param) {
if (param == null) {
success(new PaymentsCache(context).getConsumableValue("ticket", mSku));
} else {
error(param);
}
}
abstract protected void success(String ticket);
abstract protected void error(String message);
}

View File

@@ -0,0 +1,93 @@
/*************************************************************************/
/* HandlePurchaseTask.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.payments;
import android.app.Activity;
import android.content.Intent;
import org.json.JSONException;
import org.json.JSONObject;
abstract public class HandlePurchaseTask {
private Activity context;
public HandlePurchaseTask(Activity context) {
this.context = context;
}
public void handlePurchaseRequest(int resultCode, Intent data) {
//Log.d("XXX", "Handling purchase response");
if (resultCode == Activity.RESULT_OK) {
try {
//int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
PaymentsCache pc = new PaymentsCache(context);
String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
//Log.d("XXX", "Purchase data:" + purchaseData);
String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");
//Log.d("XXX", "Purchase signature:" + dataSignature);
//Log.d("SARLANGA", purchaseData);
JSONObject jo = new JSONObject(purchaseData);
//String sku = jo.getString("productId");
//alert("You have bought the " + sku + ". Excellent choice, aventurer!");
//String orderId = jo.getString("orderId");
//String packageName = jo.getString("packageName");
String productId = jo.getString("productId");
//Long purchaseTime = jo.getLong("purchaseTime");
//Integer state = jo.getInt("purchaseState");
String developerPayload = jo.getString("developerPayload");
String purchaseToken = jo.getString("purchaseToken");
if (!pc.getConsumableValue("validation_hash", productId).equals(developerPayload)) {
error("Untrusted callback");
return;
}
//Log.d("XXX", "Este es el product ID:" + productId);
pc.setConsumableValue("ticket_signautre", productId, dataSignature);
pc.setConsumableValue("ticket", productId, purchaseData);
pc.setConsumableFlag("block", productId, true);
pc.setConsumableValue("token", productId, purchaseToken);
success(productId, dataSignature, purchaseData);
return;
} catch (JSONException e) {
error(e.getMessage());
}
} else if (resultCode == Activity.RESULT_CANCELED) {
canceled();
}
}
abstract protected void success(String sku, String signature, String ticket);
abstract protected void error(String message);
abstract protected void canceled();
}

View File

@@ -0,0 +1,72 @@
/*************************************************************************/
/* PaymentsCache.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.payments;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
public class PaymentsCache {
public Context context;
public PaymentsCache(Context context) {
this.context = context;
}
public void setConsumableFlag(String set, String sku, Boolean flag) {
SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putBoolean(sku, flag);
editor.apply();
}
public boolean getConsumableFlag(String set, String sku) {
SharedPreferences sharedPref = context.getSharedPreferences(
"consumables_" + set, Context.MODE_PRIVATE);
return sharedPref.getBoolean(sku, false);
}
public void setConsumableValue(String set, String sku, String value) {
SharedPreferences sharedPref = context.getSharedPreferences("consumables_" + set, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString(sku, value);
//Log.d("XXX", "Setting asset: consumables_" + set + ":" + sku);
editor.apply();
}
public String getConsumableValue(String set, String sku) {
SharedPreferences sharedPref = context.getSharedPreferences(
"consumables_" + set, Context.MODE_PRIVATE);
//Log.d("XXX", "Getting asset: consumables_" + set + ":" + sku);
return sharedPref.getString(sku, null);
}
}

View File

@@ -0,0 +1,419 @@
/*************************************************************************/
/* PaymentsManager.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.payments;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import com.android.vending.billing.IInAppBillingService;
import java.util.ArrayList;
import java.util.Arrays;
import org.godotengine.godot.GodotPaymentV3;
import org.json.JSONException;
import org.json.JSONObject;
public class PaymentsManager {
public static final int BILLING_RESPONSE_RESULT_OK = 0;
public static final int REQUEST_CODE_FOR_PURCHASE = 0x1001;
private static boolean auto_consume = true;
private Activity activity;
IInAppBillingService mService;
public void setActivity(Activity activity) {
this.activity = activity;
}
public static PaymentsManager createManager(Activity activity) {
PaymentsManager manager = new PaymentsManager(activity);
return manager;
}
private PaymentsManager(Activity activity) {
this.activity = activity;
}
public PaymentsManager initService() {
Intent intent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
intent.setPackage("com.android.vending");
activity.bindService(
intent,
mServiceConn,
Context.BIND_AUTO_CREATE);
return this;
}
public void destroy() {
if (mService != null) {
activity.unbindService(mServiceConn);
}
}
ServiceConnection mServiceConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
mService = null;
// At this stage, godotPaymentV3 might not have been initialized yet.
if (godotPaymentV3 != null) {
godotPaymentV3.callbackDisconnected();
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IInAppBillingService.Stub.asInterface(service);
// At this stage, godotPaymentV3 might not have been initialized yet.
if (godotPaymentV3 != null) {
godotPaymentV3.callbackConnected();
}
}
};
public void requestPurchase(final String sku, String transactionId) {
new PurchaseTask(mService, activity) {
@Override
protected void error(String message) {
godotPaymentV3.callbackFail(message);
}
@Override
protected void canceled() {
godotPaymentV3.callbackCancel();
}
@Override
protected void alreadyOwned() {
godotPaymentV3.callbackAlreadyOwned(sku);
}
}
.purchase(sku, transactionId);
}
public boolean isConnected() {
return mService != null;
}
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) {
Log.d("godot", "consumeUnconsumedPurchases :" + message);
godotPaymentV3.callbackFailConsume(message);
}
@Override
protected void notRequired() {
Log.d("godot", "callbackSuccessNoUnconsumedPurchases :");
godotPaymentV3.callbackSuccessNoUnconsumedPurchases();
}
}
.consumeItAll();
}
public void requestPurchased() {
try {
PaymentsCache pc = new PaymentsCache(activity);
String continueToken = null;
do {
Bundle bundle = mService.getPurchases(3, activity.getPackageName(), "inapp", continueToken);
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) {
godotPaymentV3.callbackPurchased("", "", "");
return;
}
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);
pc.setConsumableValue("ticket_signautre", sku, signature);
pc.setConsumableValue("ticket", sku, receipt);
pc.setConsumableFlag("block", sku, true);
pc.setConsumableValue("token", sku, token);
godotPaymentV3.callbackPurchased(receipt, signature, sku);
} catch (JSONException e) {
}
}
}
continueToken = bundle.getString("INAPP_CONTINUATION_TOKEN");
Log.d("godot", "continue token = " + continueToken);
} while (!TextUtils.isEmpty(continueToken));
} catch (Exception e) {
Log.d("godot", "Error requesting purchased products:" + e.getClass().getName() + ":" + e.getMessage());
}
}
public void processPurchaseResponse(int resultCode, Intent data) {
new HandlePurchaseTask(activity) {
@Override
protected void success(final String sku, final String signature, final String ticket) {
godotPaymentV3.callbackSuccess(ticket, signature, sku);
if (auto_consume) {
new ConsumeTask(mService, activity) {
@Override
protected void success(String ticket) {
}
@Override
protected void error(String message) {
godotPaymentV3.callbackFail(message);
}
}
.consume(sku);
}
}
@Override
protected void error(String message) {
godotPaymentV3.callbackFail(message);
}
@Override
protected void canceled() {
godotPaymentV3.callbackCancel();
}
}
.handlePurchaseRequest(resultCode, data);
}
public void validatePurchase(String purchaseToken, final String sku) {
new ValidateTask(activity, godotPaymentV3) {
@Override
protected void success() {
new ConsumeTask(mService, activity) {
@Override
protected void success(String ticket) {
godotPaymentV3.callbackSuccess(ticket, null, sku);
}
@Override
protected void error(String message) {
godotPaymentV3.callbackFail(message);
}
}
.consume(sku);
}
@Override
protected void error(String message) {
godotPaymentV3.callbackFail(message);
}
@Override
protected void canceled() {
godotPaymentV3.callbackCancel();
}
}
.validatePurchase(sku);
}
public void setAutoConsume(boolean autoConsume) {
auto_consume = autoConsume;
}
public void consume(final String sku) {
new ConsumeTask(mService, activity) {
@Override
protected void success(String ticket) {
godotPaymentV3.callbackSuccessProductMassConsumed(ticket, "", sku);
}
@Override
protected void error(String message) {
godotPaymentV3.callbackFailConsume(message);
}
}
.consume(sku);
}
// Workaround to bug where sometimes response codes come as Long instead of Integer
int getResponseCodeFromBundle(Bundle b) {
Object o = b.get("RESPONSE_CODE");
if (o == null) {
//logDebug("Bundle with null response code, assuming OK (known issue)");
return BILLING_RESPONSE_RESULT_OK;
} else if (o instanceof Integer)
return ((Integer)o).intValue();
else if (o instanceof Long)
return (int)((Long)o).longValue();
else {
//logError("Unexpected type for bundle response code.");
//logError(o.getClass().getName());
throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
}
}
/**
* Returns a human-readable description for the given response code.
*
* @param code The response code
* @return A human-readable string explaining the result code.
* It also includes the result code numerically.
*/
public static String getResponseDesc(int code) {
String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/"
+
"3:Billing Unavailable/4:Item unavailable/"
+
"5:Developer Error/6:Error/7:Item Already Owned/"
+
"8:Item not owned")
.split("/");
String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/"
+
"-1002:Bad response received/"
+
"-1003:Purchase signature verification failed/"
+
"-1004:Send intent failed/"
+
"-1005:User cancelled/"
+
"-1006:Unknown purchase response/"
+
"-1007:Missing token/"
+
"-1008:Unknown error/"
+
"-1009:Subscriptions not available/"
+
"-1010:Invalid consumption attempt")
.split("/");
if (code <= -1000) {
int index = -1000 - code;
if (index >= 0 && index < iabhelper_msgs.length)
return iabhelper_msgs[index];
else
return String.valueOf(code) + ":Unknown IAB Helper Error";
} else if (code < 0 || code >= iab_msgs.length)
return String.valueOf(code) + ":Unknown";
else
return iab_msgs[code];
}
public void querySkuDetails(final String[] list) {
(new Thread(new Runnable() {
@Override
public void run() {
ArrayList<String> skuList = new ArrayList<String>(Arrays.asList(list));
if (skuList.size() == 0) {
return;
}
// Split the sku list in blocks of no more than 20 elements.
ArrayList<ArrayList<String>> packs = new ArrayList<ArrayList<String>>();
ArrayList<String> tempList;
int n = skuList.size() / 20;
int mod = skuList.size() % 20;
for (int i = 0; i < n; i++) {
tempList = new ArrayList<String>();
for (String s : skuList.subList(i * 20, i * 20 + 20)) {
tempList.add(s);
}
packs.add(tempList);
}
if (mod != 0) {
tempList = new ArrayList<String>();
for (String s : skuList.subList(n * 20, n * 20 + mod)) {
tempList.add(s);
}
packs.add(tempList);
}
for (ArrayList<String> skuPartList : packs) {
Bundle querySkus = new Bundle();
querySkus.putStringArrayList("ITEM_ID_LIST", skuPartList);
Bundle skuDetails = null;
try {
skuDetails = mService.getSkuDetails(3, activity.getPackageName(), "inapp", querySkus);
if (!skuDetails.containsKey("DETAILS_LIST")) {
int response = getResponseCodeFromBundle(skuDetails);
if (response != BILLING_RESPONSE_RESULT_OK) {
godotPaymentV3.errorSkuDetail(getResponseDesc(response));
} else {
godotPaymentV3.errorSkuDetail("No error but no detail list.");
}
return;
}
ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST");
for (String thisResponse : responseList) {
Log.d("godot", "response = " + thisResponse);
godotPaymentV3.addSkuDetail(thisResponse);
}
} catch (RemoteException e) {
e.printStackTrace();
godotPaymentV3.errorSkuDetail("RemoteException error!");
}
}
godotPaymentV3.completeSkuDetail();
}
}))
.start();
}
private GodotPaymentV3 godotPaymentV3;
public void setBaseSingleton(GodotPaymentV3 godotPaymentV3) {
this.godotPaymentV3 = godotPaymentV3;
}
}

View File

@@ -0,0 +1,118 @@
/*************************************************************************/
/* PurchaseTask.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.payments;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
import com.android.vending.billing.IInAppBillingService;
abstract public class PurchaseTask {
private Activity context;
private IInAppBillingService mService;
public PurchaseTask(IInAppBillingService mService, Activity context) {
this.context = context;
this.mService = mService;
}
private boolean isLooping = false;
public void purchase(final String sku, final String transactionId) {
Log.d("XXX", "Starting purchase for: " + sku);
PaymentsCache pc = new PaymentsCache(context);
Boolean isBlocked = pc.getConsumableFlag("block", sku);
/*
if(isBlocked){
Log.d("XXX", "Is awaiting payment confirmation");
error("Awaiting payment confirmation");
return;
}
*/
final String hash = transactionId;
Bundle buyIntentBundle;
try {
buyIntentBundle = mService.getBuyIntent(3, context.getApplicationContext().getPackageName(), sku, "inapp", hash);
} catch (RemoteException e) {
//Log.d("XXX", "Error: " + e.getMessage());
error(e.getMessage());
return;
}
Object rc = buyIntentBundle.get("RESPONSE_CODE");
int responseCode = 0;
if (rc == null) {
responseCode = PaymentsManager.BILLING_RESPONSE_RESULT_OK;
} else if (rc instanceof Integer) {
responseCode = ((Integer)rc).intValue();
} else if (rc instanceof Long) {
responseCode = (int)((Long)rc).longValue();
}
//Log.d("XXX", "Buy intent response code: " + responseCode);
if (responseCode == 1 || responseCode == 3 || responseCode == 4) {
canceled();
return;
}
if (responseCode == 7) {
alreadyOwned();
return;
}
PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");
pc.setConsumableValue("validation_hash", sku, hash);
try {
if (context == null) {
//Log.d("XXX", "No context!");
}
if (pendingIntent == null) {
//Log.d("XXX", "No pending intent");
}
//Log.d("XXX", "Starting activity for purchase!");
context.startIntentSenderForResult(
pendingIntent.getIntentSender(),
PaymentsManager.REQUEST_CODE_FOR_PURCHASE,
new Intent(),
Integer.valueOf(0), Integer.valueOf(0),
Integer.valueOf(0));
} catch (SendIntentException e) {
error(e.getMessage());
}
}
abstract protected void error(String message);
abstract protected void canceled();
abstract protected void alreadyOwned();
}

View File

@@ -0,0 +1,141 @@
/*************************************************************************/
/* ReleaseAllConsumablesTask.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.payments;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import com.android.vending.billing.IInAppBillingService;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import org.json.JSONException;
import org.json.JSONObject;
abstract public class ReleaseAllConsumablesTask {
private Context context;
private IInAppBillingService mService;
private static class ReleaseAllConsumablesAsyncTask extends AsyncTask<String, String, String> {
private WeakReference<ReleaseAllConsumablesTask> mTask;
private String mSku;
private String mReceipt;
private String mSignature;
private String mToken;
ReleaseAllConsumablesAsyncTask(ReleaseAllConsumablesTask task, String sku, String receipt, String signature, String token) {
mTask = new WeakReference<ReleaseAllConsumablesTask>(task);
mSku = sku;
mReceipt = receipt;
mSignature = signature;
mToken = token;
}
@Override
protected String doInBackground(String... params) {
ReleaseAllConsumablesTask consume = mTask.get();
if (consume != null) {
return consume.doInBackground(mToken);
}
return null;
}
@Override
protected void onPostExecute(String param) {
ReleaseAllConsumablesTask consume = mTask.get();
if (consume != null) {
consume.success(mSku, mReceipt, mSignature, mToken);
}
}
}
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);
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 ReleaseAllConsumablesAsyncTask(this, sku, receipt, signature, token).execute();
} catch (JSONException e) {
}
}
}
} catch (Exception e) {
Log.d("godot", "Error releasing products:" + e.getClass().getName() + ":" + e.getMessage());
}
}
private String doInBackground(String token) {
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;
}
abstract protected void success(String sku, String receipt, String signature, String token);
abstract protected void error(String message);
abstract protected void notRequired();
}

View File

@@ -0,0 +1,142 @@
/*************************************************************************/
/* ValidateTask.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.payments;
import android.app.Activity;
import android.app.ProgressDialog;
import android.os.AsyncTask;
import java.lang.ref.WeakReference;
import org.godotengine.godot.GodotPaymentV3;
import org.godotengine.godot.utils.HttpRequester;
import org.godotengine.godot.utils.RequestParams;
import org.json.JSONException;
import org.json.JSONObject;
abstract public class ValidateTask {
private Activity context;
private GodotPaymentV3 godotPaymentsV3;
private ProgressDialog dialog;
private String mSku;
private static class ValidateAsyncTask extends AsyncTask<String, String, String> {
private WeakReference<ValidateTask> mTask;
ValidateAsyncTask(ValidateTask task) {
mTask = new WeakReference<>(task);
}
@Override
protected void onPreExecute() {
ValidateTask task = mTask.get();
if (task != null) {
task.onPreExecute();
}
}
@Override
protected String doInBackground(String... params) {
ValidateTask task = mTask.get();
if (task != null) {
return task.doInBackground(params);
}
return null;
}
@Override
protected void onPostExecute(String response) {
ValidateTask task = mTask.get();
if (task != null) {
task.onPostExecute(response);
}
}
}
public ValidateTask(Activity context, GodotPaymentV3 godotPaymentsV3) {
this.context = context;
this.godotPaymentsV3 = godotPaymentsV3;
}
public void validatePurchase(final String sku) {
mSku = sku;
new ValidateAsyncTask(this).execute();
}
private void onPreExecute() {
dialog = ProgressDialog.show(context, null, "Please wait...");
}
private String doInBackground(String... params) {
PaymentsCache pc = new PaymentsCache(context);
String url = godotPaymentsV3.getPurchaseValidationUrlPrefix();
RequestParams param = new RequestParams();
param.setUrl(url);
param.put("ticket", pc.getConsumableValue("ticket", mSku));
param.put("purchaseToken", pc.getConsumableValue("token", mSku));
param.put("sku", mSku);
//Log.d("XXX", "Haciendo request a " + url);
//Log.d("XXX", "ticket: " + pc.getConsumableValue("ticket", sku));
//Log.d("XXX", "purchaseToken: " + pc.getConsumableValue("token", sku));
//Log.d("XXX", "sku: " + sku);
param.put("package", context.getApplicationContext().getPackageName());
HttpRequester requester = new HttpRequester();
String jsonResponse = requester.post(param);
//Log.d("XXX", "Validation response:\n"+jsonResponse);
return jsonResponse;
}
private void onPostExecute(String response) {
if (dialog != null) {
dialog.dismiss();
dialog = null;
}
JSONObject j;
try {
j = new JSONObject(response);
if (j.getString("status").equals("OK")) {
success();
return;
} else if (j.getString("status") != null) {
error(j.getString("message"));
} else {
error("Connection error");
}
} catch (JSONException e) {
error(e.getMessage());
} catch (Exception e) {
error(e.getMessage());
}
}
abstract protected void success();
abstract protected void error(String message);
abstract protected void canceled();
}

View File

@@ -0,0 +1,69 @@
/*************************************************************************/
/* Crypt.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.utils;
import java.security.MessageDigest;
import java.util.Random;
public class Crypt {
public static String md5(String input) {
try {
// Create MD5 Hash
MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
digest.update(input.getBytes());
byte messageDigest[] = digest.digest();
// Create Hex String
StringBuffer hexString = new StringBuffer();
for (int i = 0; i < messageDigest.length; i++)
hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
return hexString.toString();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static String createRandomHash() {
return md5(Long.toString(createRandomLong()));
}
public static long createAbsRandomLong() {
return Math.abs(createRandomLong());
}
public static long createRandomLong() {
Random r = new Random();
return r.nextLong();
}
}

View File

@@ -0,0 +1,69 @@
/*************************************************************************/
/* CustomSSLSocketFactory.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.utils;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
/**
*
* @author Luis Linietsky <luis.linietsky@gmail.com>
*/
public class CustomSSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
public CustomSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(truststore);
sslContext.init(null, tmf.getTrustManagers(), null);
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
}
@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
}

View File

@@ -0,0 +1,157 @@
/*************************************************************************/
/* GLUtils.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.utils;
import android.util.Log;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
/**
* Contains GL utilities methods.
*/
public class GLUtils {
private static final String TAG = GLUtils.class.getSimpleName();
public static final boolean DEBUG = false;
public static boolean use_gl3 = false;
public static boolean use_32 = false;
public static boolean use_debug_opengl = false;
private static final String[] ATTRIBUTES_NAMES = new String[] {
"EGL_BUFFER_SIZE",
"EGL_ALPHA_SIZE",
"EGL_BLUE_SIZE",
"EGL_GREEN_SIZE",
"EGL_RED_SIZE",
"EGL_DEPTH_SIZE",
"EGL_STENCIL_SIZE",
"EGL_CONFIG_CAVEAT",
"EGL_CONFIG_ID",
"EGL_LEVEL",
"EGL_MAX_PBUFFER_HEIGHT",
"EGL_MAX_PBUFFER_PIXELS",
"EGL_MAX_PBUFFER_WIDTH",
"EGL_NATIVE_RENDERABLE",
"EGL_NATIVE_VISUAL_ID",
"EGL_NATIVE_VISUAL_TYPE",
"EGL_PRESERVED_RESOURCES",
"EGL_SAMPLES",
"EGL_SAMPLE_BUFFERS",
"EGL_SURFACE_TYPE",
"EGL_TRANSPARENT_TYPE",
"EGL_TRANSPARENT_RED_VALUE",
"EGL_TRANSPARENT_GREEN_VALUE",
"EGL_TRANSPARENT_BLUE_VALUE",
"EGL_BIND_TO_TEXTURE_RGB",
"EGL_BIND_TO_TEXTURE_RGBA",
"EGL_MIN_SWAP_INTERVAL",
"EGL_MAX_SWAP_INTERVAL",
"EGL_LUMINANCE_SIZE",
"EGL_ALPHA_MASK_SIZE",
"EGL_COLOR_BUFFER_TYPE",
"EGL_RENDERABLE_TYPE",
"EGL_CONFORMANT"
};
private static final int[] ATTRIBUTES = new int[] {
EGL10.EGL_BUFFER_SIZE,
EGL10.EGL_ALPHA_SIZE,
EGL10.EGL_BLUE_SIZE,
EGL10.EGL_GREEN_SIZE,
EGL10.EGL_RED_SIZE,
EGL10.EGL_DEPTH_SIZE,
EGL10.EGL_STENCIL_SIZE,
EGL10.EGL_CONFIG_CAVEAT,
EGL10.EGL_CONFIG_ID,
EGL10.EGL_LEVEL,
EGL10.EGL_MAX_PBUFFER_HEIGHT,
EGL10.EGL_MAX_PBUFFER_PIXELS,
EGL10.EGL_MAX_PBUFFER_WIDTH,
EGL10.EGL_NATIVE_RENDERABLE,
EGL10.EGL_NATIVE_VISUAL_ID,
EGL10.EGL_NATIVE_VISUAL_TYPE,
0x3030, // EGL10.EGL_PRESERVED_RESOURCES,
EGL10.EGL_SAMPLES,
EGL10.EGL_SAMPLE_BUFFERS,
EGL10.EGL_SURFACE_TYPE,
EGL10.EGL_TRANSPARENT_TYPE,
EGL10.EGL_TRANSPARENT_RED_VALUE,
EGL10.EGL_TRANSPARENT_GREEN_VALUE,
EGL10.EGL_TRANSPARENT_BLUE_VALUE,
0x3039, // EGL10.EGL_BIND_TO_TEXTURE_RGB,
0x303A, // EGL10.EGL_BIND_TO_TEXTURE_RGBA,
0x303B, // EGL10.EGL_MIN_SWAP_INTERVAL,
0x303C, // EGL10.EGL_MAX_SWAP_INTERVAL,
EGL10.EGL_LUMINANCE_SIZE,
EGL10.EGL_ALPHA_MASK_SIZE,
EGL10.EGL_COLOR_BUFFER_TYPE,
EGL10.EGL_RENDERABLE_TYPE,
0x3042 // EGL10.EGL_CONFORMANT
};
private GLUtils() {}
public static void checkEglError(String tag, String prompt, EGL10 egl) {
int error;
while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) {
Log.e(tag, String.format("%s: EGL error: 0x%x", prompt, error));
}
}
public static void printConfigs(EGL10 egl, EGLDisplay display,
EGLConfig[] configs) {
int numConfigs = configs.length;
Log.v(TAG, String.format("%d configurations", numConfigs));
for (int i = 0; i < numConfigs; i++) {
Log.v(TAG, String.format("Configuration %d:\n", i));
printConfig(egl, display, configs[i]);
}
}
private static void printConfig(EGL10 egl, EGLDisplay display,
EGLConfig config) {
int[] value = new int[1];
for (int i = 0; i < ATTRIBUTES.length; i++) {
int attribute = ATTRIBUTES[i];
String name = ATTRIBUTES_NAMES[i];
if (egl.eglGetConfigAttrib(display, config, attribute, value)) {
Log.i(TAG, String.format(" %s: %d\n", name, value[0]));
} else {
// Log.w(TAG, String.format(" %s: failed\n", name));
while (egl.eglGetError() != EGL10.EGL_SUCCESS)
;
}
}
}
}

View File

@@ -0,0 +1,227 @@
/*************************************************************************/
/* HttpRequester.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.security.KeyStore;
import java.util.Date;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
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.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
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.HTTP;
import org.apache.http.util.EntityUtils;
/**
*
* @author Luis Linietsky <luis.linietsky@gmail.com>
*/
public class HttpRequester {
private Context context;
private static final int TTL = 600000; // 10 minutos
private long cttl = 0;
public HttpRequester() {
//Log.d("XXX", "Creando http request sin contexto");
}
public HttpRequester(Context context) {
this.context = context;
//Log.d("XXX", "Creando http request con contexto");
}
public String post(RequestParams params) {
HttpPost httppost = new HttpPost(params.getUrl());
try {
httppost.setEntity(new UrlEncodedFormEntity(params.toPairsList()));
return request(httppost);
} catch (UnsupportedEncodingException e) {
return null;
}
}
public String get(RequestParams params) {
String response = getResponseFromCache(params.getUrl());
if (response == null) {
//Log.d("XXX", "Cache miss!");
HttpGet httpget = new HttpGet(params.getUrl());
long timeInit = new Date().getTime();
response = request(httpget);
long delay = new Date().getTime() - timeInit;
Log.d("HttpRequest::get(url)", "Url: " + params.getUrl() + " downloaded in " + String.format("%.03f", delay / 1000.0f) + " seconds");
if (response == null || response.length() == 0) {
response = "";
} else {
saveResponseIntoCache(params.getUrl(), response);
}
}
Log.d("XXX", "Req: " + params.getUrl());
Log.d("XXX", "Resp: " + response);
return response;
}
private String request(HttpUriRequest request) {
//Log.d("XXX", "Haciendo request a: " + request.getURI() );
Log.d("PPP", "Haciendo request a: " + request.getURI());
long init = new Date().getTime();
HttpClient httpclient = getNewHttpClient();
HttpParams httpParameters = httpclient.getParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, 0);
HttpConnectionParams.setSoTimeout(httpParameters, 0);
HttpConnectionParams.setTcpNoDelay(httpParameters, true);
try {
HttpResponse response = httpclient.execute(request);
Log.d("PPP", "Fin de request (" + (new Date().getTime() - init) + ") a: " + request.getURI());
//Log.d("XXX1", "Status:" + response.getStatusLine().toString());
if (response.getStatusLine().getStatusCode() == 200) {
String strResponse = EntityUtils.toString(response.getEntity());
//Log.d("XXX2", strResponse);
return strResponse;
} else {
Log.d("XXX3", "Response status code:" + response.getStatusLine().getStatusCode() + "\n" + EntityUtils.toString(response.getEntity()));
return null;
}
} catch (ClientProtocolException e) {
Log.d("XXX3", e.getMessage());
} catch (IOException e) {
Log.d("XXX4", e.getMessage());
}
return null;
}
private HttpClient getNewHttpClient() {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
SSLSocketFactory sf = new CustomSSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", sf, 443));
ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);
return new DefaultHttpClient(ccm, params);
} catch (Exception e) {
return new DefaultHttpClient();
}
}
private static String convertStreamToString(InputStream is) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line = null;
try {
while ((line = reader.readLine()) != null) {
sb.append((line + "\n"));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}
public void saveResponseIntoCache(String request, String response) {
if (context == null) {
//Log.d("XXX", "No context, cache failed!");
return;
}
SharedPreferences sharedPref = context.getSharedPreferences("http_get_cache", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("request_" + Crypt.md5(request), response);
editor.putLong("request_" + Crypt.md5(request) + "_ttl", new Date().getTime() + getTtl());
editor.apply();
}
public String getResponseFromCache(String request) {
if (context == null) {
Log.d("XXX", "No context, cache miss");
return null;
}
SharedPreferences sharedPref = context.getSharedPreferences("http_get_cache", Context.MODE_PRIVATE);
long ttl = getResponseTtl(request);
if (ttl == 0l || (new Date().getTime() - ttl) > 0l) {
Log.d("XXX", "Cache invalid ttl:" + ttl + " vs now:" + new Date().getTime());
return null;
}
return sharedPref.getString("request_" + Crypt.md5(request), null);
}
public long getResponseTtl(String request) {
SharedPreferences sharedPref = context.getSharedPreferences(
"http_get_cache", Context.MODE_PRIVATE);
return sharedPref.getLong("request_" + Crypt.md5(request) + "_ttl", 0l);
}
public long getTtl() {
return cttl > 0 ? cttl : TTL;
}
public void setTtl(long ttl) {
this.cttl = (ttl * 1000) + new Date().getTime();
}
}

View File

@@ -0,0 +1,85 @@
/*************************************************************************/
/* RequestParams.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.utils;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
/**
*
* @author Luis Linietsky <luis.linietsky@gmail.com>
*/
public class RequestParams {
private HashMap<String, String> params;
private String url;
public RequestParams() {
params = new HashMap<String, String>();
}
public void put(String key, String value) {
params.put(key, value);
}
public String get(String key) {
return params.get(key);
}
public void remove(Object key) {
params.remove(key);
}
public boolean has(String key) {
return params.containsKey(key);
}
public List<NameValuePair> toPairsList() {
List<NameValuePair> fields = new ArrayList<NameValuePair>();
for (String key : params.keySet()) {
fields.add(new BasicNameValuePair(key, this.get(key)));
}
return fields;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}

View File

@@ -0,0 +1,51 @@
/*************************************************************************/
/* XRMode.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.xr;
/**
* Godot available XR modes.
*/
public enum XRMode {
REGULAR(0, "Regular", "--xr_mode_regular", "Default Android Gamepad"), // Regular/flatscreen
OVR(1, "Oculus Mobile VR", "--xr_mode_ovr", "");
final int index;
final String label;
public final String cmdLineArg;
public final String inputFallbackMapping;
XRMode(int index, String label, String cmdLineArg, String inputFallbackMapping) {
this.index = index;
this.label = label;
this.cmdLineArg = cmdLineArg;
this.inputFallbackMapping = inputFallbackMapping;
}
}

View File

@@ -0,0 +1,112 @@
/*************************************************************************/
/* OvrConfigChooser.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.xr.ovr;
import android.opengl.EGLExt;
import android.opengl.GLSurfaceView;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
/**
* EGL config chooser for the Oculus Mobile VR SDK.
*/
public class OvrConfigChooser implements GLSurfaceView.EGLConfigChooser {
private static final int[] CONFIG_ATTRIBS = {
EGL10.EGL_RED_SIZE, 8,
EGL10.EGL_GREEN_SIZE, 8,
EGL10.EGL_BLUE_SIZE, 8,
EGL10.EGL_ALPHA_SIZE, 8, // Need alpha for the multi-pass timewarp compositor
EGL10.EGL_DEPTH_SIZE, 0,
EGL10.EGL_STENCIL_SIZE, 0,
EGL10.EGL_SAMPLES, 0,
EGL10.EGL_NONE
};
@Override
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
// Do NOT use eglChooseConfig, because the Android EGL code pushes in
// multisample flags in eglChooseConfig if the user has selected the "force 4x
// MSAA" option in settings, and that is completely wasted for our warp
// target.
int[] numConfig = new int[1];
if (!egl.eglGetConfigs(display, null, 0, numConfig)) {
throw new IllegalArgumentException("eglGetConfigs failed.");
}
int configsCount = numConfig[0];
if (configsCount <= 0) {
throw new IllegalArgumentException("No configs match configSpec");
}
EGLConfig[] configs = new EGLConfig[configsCount];
if (!egl.eglGetConfigs(display, configs, configsCount, numConfig)) {
throw new IllegalArgumentException("eglGetConfigs #2 failed.");
}
int[] value = new int[1];
for (EGLConfig config : configs) {
egl.eglGetConfigAttrib(display, config, EGL10.EGL_RENDERABLE_TYPE, value);
if ((value[0] & EGLExt.EGL_OPENGL_ES3_BIT_KHR) != EGLExt.EGL_OPENGL_ES3_BIT_KHR) {
continue;
}
// The pbuffer config also needs to be compatible with normal window rendering
// so it can share textures with the window context.
egl.eglGetConfigAttrib(display, config, EGL10.EGL_SURFACE_TYPE, value);
if ((value[0] & (EGL10.EGL_WINDOW_BIT | EGL10.EGL_PBUFFER_BIT)) != (EGL10.EGL_WINDOW_BIT | EGL10.EGL_PBUFFER_BIT)) {
continue;
}
// Check each attribute in CONFIG_ATTRIBS (which are the attributes we care about)
// and ensure the value in config matches.
int attribIndex = 0;
while (CONFIG_ATTRIBS[attribIndex] != EGL10.EGL_NONE) {
egl.eglGetConfigAttrib(display, config, CONFIG_ATTRIBS[attribIndex], value);
if (value[0] != CONFIG_ATTRIBS[attribIndex + 1]) {
// Attribute key's value does not match the configs value.
// Start checking next config.
break;
}
// Step by two because CONFIG_ATTRIBS is in key/value pairs.
attribIndex += 2;
}
if (CONFIG_ATTRIBS[attribIndex] == EGL10.EGL_NONE) {
// All relevant attributes match, set the config and stop checking the rest.
return config;
}
}
return null;
}
}

View File

@@ -0,0 +1,58 @@
/*************************************************************************/
/* OvrContextFactory.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.xr.ovr;
import android.opengl.EGL14;
import android.opengl.GLSurfaceView;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
/**
* EGL Context factory for the Oculus mobile VR SDK.
*/
public class OvrContextFactory implements GLSurfaceView.EGLContextFactory {
private static final int[] CONTEXT_ATTRIBS = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE
};
@Override
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
return egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, CONTEXT_ATTRIBS);
}
@Override
public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
egl.eglDestroyContext(display, context);
}
}

View File

@@ -0,0 +1,60 @@
/*************************************************************************/
/* OvrWindowSurfaceFactory.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.xr.ovr;
import android.opengl.GLSurfaceView;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
/**
* EGL window surface factory for the Oculus mobile VR SDK.
*/
public class OvrWindowSurfaceFactory implements GLSurfaceView.EGLWindowSurfaceFactory {
private final static int[] SURFACE_ATTRIBS = {
EGL10.EGL_WIDTH, 16,
EGL10.EGL_HEIGHT, 16,
EGL10.EGL_NONE
};
@Override
public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config,
Object nativeWindow) {
return egl.eglCreatePbufferSurface(display, config, SURFACE_ATTRIBS);
}
@Override
public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
egl.eglDestroySurface(display, surface);
}
}

View File

@@ -0,0 +1,151 @@
/*************************************************************************/
/* RegularConfigChooser.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.xr.regular;
import android.opengl.GLSurfaceView;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
import org.godotengine.godot.utils.GLUtils;
/**
* Used to select the egl config for pancake games.
*/
public class RegularConfigChooser implements GLSurfaceView.EGLConfigChooser {
private static final String TAG = RegularConfigChooser.class.getSimpleName();
private int[] mValue = new int[1];
/* This EGL config specification is used to specify 2.0 rendering.
* We use a minimum size of 4 bits for red/green/blue, but will
* perform actual matching in chooseConfig() below.
*/
private static int EGL_OPENGL_ES2_BIT = 4;
private static int[] s_configAttribs2 = {
EGL10.EGL_RED_SIZE, 4,
EGL10.EGL_GREEN_SIZE, 4,
EGL10.EGL_BLUE_SIZE, 4,
// EGL10.EGL_DEPTH_SIZE, 16,
// EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE,
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL10.EGL_NONE
};
private static int[] s_configAttribs3 = {
EGL10.EGL_RED_SIZE, 4,
EGL10.EGL_GREEN_SIZE, 4,
EGL10.EGL_BLUE_SIZE, 4,
// EGL10.EGL_DEPTH_SIZE, 16,
// EGL10.EGL_STENCIL_SIZE, EGL10.EGL_DONT_CARE,
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //apparently there is no EGL_OPENGL_ES3_BIT
EGL10.EGL_NONE
};
public RegularConfigChooser(int r, int g, int b, int a, int depth, int stencil) {
mRedSize = r;
mGreenSize = g;
mBlueSize = b;
mAlphaSize = a;
mDepthSize = depth;
mStencilSize = stencil;
}
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
/* Get the number of minimally matching EGL configurations
*/
int[] num_config = new int[1];
egl.eglChooseConfig(display, GLUtils.use_gl3 ? s_configAttribs3 : s_configAttribs2, null, 0, num_config);
int numConfigs = num_config[0];
if (numConfigs <= 0) {
throw new IllegalArgumentException("No configs match configSpec");
}
/* Allocate then read the array of minimally matching EGL configs
*/
EGLConfig[] configs = new EGLConfig[numConfigs];
egl.eglChooseConfig(display, GLUtils.use_gl3 ? s_configAttribs3 : s_configAttribs2, configs, numConfigs, num_config);
if (GLUtils.DEBUG) {
GLUtils.printConfigs(egl, display, configs);
}
/* Now return the "best" one
*/
return chooseConfig(egl, display, configs);
}
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display,
EGLConfig[] configs) {
for (EGLConfig config : configs) {
int d = findConfigAttrib(egl, display, config,
EGL10.EGL_DEPTH_SIZE, 0);
int s = findConfigAttrib(egl, display, config,
EGL10.EGL_STENCIL_SIZE, 0);
// We need at least mDepthSize and mStencilSize bits
if (d < mDepthSize || s < mStencilSize)
continue;
// We want an *exact* match for red/green/blue/alpha
int r = findConfigAttrib(egl, display, config,
EGL10.EGL_RED_SIZE, 0);
int g = findConfigAttrib(egl, display, config,
EGL10.EGL_GREEN_SIZE, 0);
int b = findConfigAttrib(egl, display, config,
EGL10.EGL_BLUE_SIZE, 0);
int a = findConfigAttrib(egl, display, config,
EGL10.EGL_ALPHA_SIZE, 0);
if (r == mRedSize && g == mGreenSize && b == mBlueSize && a == mAlphaSize)
return config;
}
return null;
}
private int findConfigAttrib(EGL10 egl, EGLDisplay display,
EGLConfig config, int attribute, int defaultValue) {
if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) {
return mValue[0];
}
return defaultValue;
}
// Subclasses can adjust these values:
protected int mRedSize;
protected int mGreenSize;
protected int mBlueSize;
protected int mAlphaSize;
protected int mDepthSize;
protected int mStencilSize;
}

View File

@@ -0,0 +1,81 @@
/*************************************************************************/
/* RegularContextFactory.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.xr.regular;
import android.opengl.GLSurfaceView;
import android.util.Log;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import org.godotengine.godot.GodotLib;
import org.godotengine.godot.utils.GLUtils;
/**
* Factory used to setup the opengl context for pancake games.
*/
public class RegularContextFactory implements GLSurfaceView.EGLContextFactory {
private static final String TAG = RegularContextFactory.class.getSimpleName();
private static final int _EGL_CONTEXT_FLAGS_KHR = 0x30FC;
private static final int _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR = 0x00000001;
private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
String driver_name = GodotLib.getGlobal("rendering/quality/driver/driver_name");
if (GLUtils.use_gl3 && !driver_name.equals("GLES3")) {
GLUtils.use_gl3 = false;
}
if (GLUtils.use_gl3)
Log.w(TAG, "creating OpenGL ES 3.0 context :");
else
Log.w(TAG, "creating OpenGL ES 2.0 context :");
GLUtils.checkEglError(TAG, "Before eglCreateContext", egl);
EGLContext context;
if (GLUtils.use_debug_opengl) {
int[] attrib_list2 = { EGL_CONTEXT_CLIENT_VERSION, 2, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE };
int[] attrib_list3 = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE };
context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, GLUtils.use_gl3 ? attrib_list3 : attrib_list2);
} else {
int[] attrib_list2 = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
int[] attrib_list3 = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE };
context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, GLUtils.use_gl3 ? attrib_list3 : attrib_list2);
}
GLUtils.checkEglError(TAG, "After eglCreateContext", egl);
return context;
}
public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
egl.eglDestroyContext(display, context);
}
}

View File

@@ -0,0 +1,61 @@
/*************************************************************************/
/* RegularFallbackConfigChooser.java */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.xr.regular;
import android.util.Log;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
import org.godotengine.godot.utils.GLUtils;
/* Fallback if 32bit View is not supported*/
public class RegularFallbackConfigChooser extends RegularConfigChooser {
private static final String TAG = RegularFallbackConfigChooser.class.getSimpleName();
private RegularConfigChooser fallback;
public RegularFallbackConfigChooser(int r, int g, int b, int a, int depth, int stencil, RegularConfigChooser fallback) {
super(r, g, b, a, depth, stencil);
this.fallback = fallback;
}
@Override
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) {
EGLConfig ec = super.chooseConfig(egl, display, configs);
if (ec == null) {
Log.w(TAG, "Trying ConfigChooser fallback");
ec = fallback.chooseConfig(egl, display, configs);
GLUtils.use_32 = false;
}
return ec;
}
}