/*
* Copyright 2011 - AndroidQuery.com (tinyeeliu@gmail.com)
*
* 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.smartandroid.sa.aq;
import java.util.Locale;
import org.json.JSONObject;
import org.xml.sax.XMLReader;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.Html;
import android.text.Html.TagHandler;
public class MarketService {
private Activity act;
private AQuery aq;
private Handler handler;
private String locale;
private String rateUrl;
private String updateUrl;
private boolean force;
private int progress;
private long expire = 12 * 60 * 1000;
private String version;
private boolean fetch;
private boolean completed;
private int level = REVISION;
/** Update check level REVISION. */
public static final int REVISION = 0;
/** Update check level MINOR. */
public static final int MINOR = 1;
/** Update check level MAJOR. */
public static final int MAJOR = 2;
/**
* Instantiates a new MarketService.
*
* @param act
* Current activity.
*/
public MarketService(Activity act) {
this.act = act;
this.aq = new AQuery(act);
this.handler = new Handler();
this.locale = Locale.getDefault().toString();
this.rateUrl = getMarketUrl();
this.updateUrl = rateUrl;
}
/**
* Set the destination url of the default rate/review button.
*
* @param url
* url
* @return self
*/
public MarketService rateUrl(String url) {
this.rateUrl = url;
return this;
}
/**
* Set the update check granularity level. Default is REVISION.
*
* <br>
*
* Can be REVISION, MINOR, or MAJOR.
*
* <br>
*
* App version format: MAJOR.MINOR.REVISION
*
* <br>
*
* Example:
*
* <br>
* Current app version: 3.1.2 <br>
* Newest app version: 3.1.4 <br>
* Update notice will show if level is REVISION, because the revision code
* is higher. <br>
* Update notice will NOT show if level is MINOR, because the minor code is
* equal (or higher).
*
*
* @param level
* granularity level
* @return self
*/
public MarketService level(int level) {
this.level = level;
return this;
}
/**
* Set the destination url of the default update button.
*
* @param url
* url
* @return self
*/
public MarketService updateUrl(String url) {
this.updateUrl = url;
return this;
}
/**
* Force the update dialog to a specific locale. Example: en_US, ja_JP.
*
* @param locale
* interface locale
* @return self
*/
public MarketService locale(String locale) {
this.locale = locale;
return this;
}
/**
* Display a progress view during version check.
*
* @param id
* view id
* @return self
*/
public MarketService progress(int id) {
this.progress = id;
return this;
}
/**
* Force a version check against the AQuery server and show a dialog
* regardless of versions.
*
* @param force
* force an update check
* @return self
*/
public MarketService force(boolean force) {
this.force = force;
return this;
}
/**
* The time duration which last version check expires. Default is 10 hours.
*
* @param expire
* expire time in milliseconds
* @return self
*/
public MarketService expire(long expire) {
this.expire = expire;
return this;
}
private static ApplicationInfo ai;
private ApplicationInfo getApplicationInfo() {
if (ai == null) {
ai = act.getApplicationInfo();
}
return ai;
}
private static PackageInfo pi;
private PackageInfo getPackageInfo() {
if (pi == null) {
try {
pi = act.getPackageManager().getPackageInfo(getAppId(), 0);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
}
return pi;
}
private String getHost() {
return "https://androidquery.appspot.com";
}
private String getQueryUrl() {
String appId = getAppId();
String url = getHost() + "/api/market?app=" + appId + "&locale="
+ locale + "&version=" + getVersion() + "&code="
+ getVersionCode() + "&aq=" + AQuery.VERSION;
if (force) {
url += "&force=true";
}
return url;
}
private String getAppId() {
return getApplicationInfo().packageName;
}
private Drawable getAppIcon() {
Drawable d = getApplicationInfo().loadIcon(act.getPackageManager());
return d;
}
private String getVersion() {
return getPackageInfo().versionName;
}
private int getVersionCode() {
return getPackageInfo().versionCode;
}
/**
* Perform a version check.
*
*
*/
public void checkVersion() {
String url = getQueryUrl();
AjaxCallback<JSONObject> cb = new AjaxCallback<JSONObject>();
cb.url(url).type(JSONObject.class).handler(handler, "marketCb")
.fileCache(!force).expire(expire);
aq.progress(progress).ajax(cb);
}
private static boolean openUrl(Activity act, String url) {
try {
if (url == null)
return false;
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
act.startActivity(intent);
return true;
} catch (Exception e) {
return false;
}
}
private String getMarketUrl() {
String id = getAppId();
return "market://details?id=" + id;
}
protected void callback(String url, JSONObject jo, AjaxStatus status) {
if (jo == null)
return;
String latestVer = jo.optString("version", "0");
int latestCode = jo.optInt("code", 0);
AQUtility.debug("version", getVersion() + "->" + latestVer + ":"
+ getVersionCode() + "->" + latestCode);
AQUtility.debug("outdated", outdated(latestVer, latestCode));
if (force || outdated(latestVer, latestCode)) {
showUpdateDialog(jo);
}
}
private boolean outdated(String latestVer, int latestCode) {
String skip = getSkipVersion(act);
if (latestVer.equals(skip)) {
return false;
}
String version = getVersion();
int code = getVersionCode();
if (!version.equals(latestVer)) {
if (code <= latestCode) {
// return true;
return requireUpdate(version, latestVer, level);
}
}
return false;
}
private boolean requireUpdate(String existVer, String latestVer, int level) {
if (existVer.equals(latestVer))
return false;
try {
String[] evs = existVer.split("\\.");
String[] lvs = latestVer.split("\\.");
if (evs.length < 3 || lvs.length < 3)
return true;
switch (level) {
case REVISION:
if (!evs[evs.length - 1].equals(lvs[lvs.length - 1])) {
return true;
}
case MINOR:
if (!evs[evs.length - 2].equals(lvs[lvs.length - 2])) {
return true;
}
case MAJOR:
if (!evs[evs.length - 3].equals(lvs[lvs.length - 3])) {
return true;
}
return false;
default:
return true;
}
} catch (Exception e) {
AQUtility.report(e);
return true;
}
}
protected void showUpdateDialog(JSONObject jo) {
if (jo == null || version != null)
return;
if (!isActive())
return;
JSONObject dia = jo.optJSONObject("dialog");
String update = dia.optString("update", "Update");
String skip = dia.optString("skip", "Skip");
String rate = dia.optString("rate", "Rate");
// String message = dia.optString("body", "");
String body = dia.optString("wbody", "");
String title = dia.optString("title", "Update Available");
AQUtility.debug("wbody", body);
version = jo.optString("version", null);
Drawable icon = getAppIcon();
Context context = act;
final AlertDialog dialog = new AlertDialog.Builder(context)
.setIcon(icon).setTitle(title).setPositiveButton(rate, handler)
.setNeutralButton(skip, handler)
.setNegativeButton(update, handler).create();
dialog.setMessage(Html.fromHtml(patchBody(body), null, handler));
aq.show(dialog);
return;
}
private static String patchBody(String body) {
return "<small>" + body + "</small>";
}
private static final String SKIP_VERSION = "aqs.skip";
private static void setSkipVersion(Context context, String version) {
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putString(SKIP_VERSION, version).commit();
}
private static String getSkipVersion(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context)
.getString(SKIP_VERSION, null);
}
private boolean isActive() {
if (act.isFinishing())
return false;
return true;
}
private static final String BULLET = "•";
private class Handler implements DialogInterface.OnClickListener,
TagHandler {
@SuppressWarnings("unused")
public void marketCb(String url, JSONObject jo, AjaxStatus status) {
if (act.isFinishing())
return;
if (jo != null) {
String s = jo.optString("status");
if ("1".equals(s)) {
if (jo.has("dialog")) {
cb(url, jo, status);
}
if (!fetch && jo.optBoolean("fetch", false)
&& status.getSource() == AjaxStatus.NETWORK) {
fetch = true;
String marketUrl = jo.optString("marketUrl", null);
AjaxCallback<String> cb = new AjaxCallback<String>();
cb.url(marketUrl).type(String.class)
.handler(this, "detailCb");
aq.progress(progress).ajax(cb);
}
} else if ("0".equals(s)) {
status.invalidate();
} else {
cb(url, jo, status);
}
} else {
cb(url, jo, status);
}
}
private void cb(String url, JSONObject jo, AjaxStatus status) {
if (!completed) {
completed = true;
progress = 0;
callback(url, jo, status);
}
}
@SuppressWarnings("unused")
public void detailCb(String url, String html, AjaxStatus status) {
if (html != null && html.length() > 1000) {
String qurl = getQueryUrl();
AjaxCallback<JSONObject> cb = new AjaxCallback<JSONObject>();
cb.url(qurl).type(JSONObject.class).handler(this, "marketCb");
cb.param("html", html);
aq.progress(progress).ajax(cb);
}
}
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case AlertDialog.BUTTON_POSITIVE:
openUrl(act, rateUrl);
break;
case AlertDialog.BUTTON_NEGATIVE:
openUrl(act, updateUrl);
break;
case AlertDialog.BUTTON_NEUTRAL:
setSkipVersion(act, version);
break;
}
}
@Override
public void handleTag(boolean opening, String tag, Editable output,
XMLReader xmlReader) {
if ("li".equals(tag)) {
if (opening) {
output.append(" ");
output.append(BULLET);
output.append(" ");
} else {
output.append("\n");
}
}
}
}
}