/* * 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"); } } } } }