/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.service; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Locale; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.app.Dialog; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Color; import android.net.Uri; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager.BadTokenException; import android.webkit.WebView; import android.widget.Button; import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; import com.timsu.astrid.R; import com.todoroo.andlib.data.Property.StringProperty; import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.RestClient; import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; import com.todoroo.astrid.dao.StoreObjectDao; import com.todoroo.astrid.dao.StoreObjectDao.StoreObjectCriteria; import com.todoroo.astrid.data.StoreObject; import com.todoroo.astrid.gtasks.GtasksPreferenceService; import com.todoroo.astrid.utility.Constants; /** * Notifies users when there are server updates * * @author Tim Su <tim@todoroo.com> * */ @SuppressWarnings("nls") public class UpdateMessageService { private static final String URL = "http://blog.astrid.com/updates"; private static final String PLUGIN_GTASKS = "gtasks"; @Autowired protected RestClient restClient; @Autowired private GtasksPreferenceService gtasksPreferenceService; @Autowired private ActFmPreferenceService actFmPreferenceService; @Autowired private AddOnService addOnService; @Autowired private StoreObjectDao storeObjectDao; private final Activity activity; public UpdateMessageService(Activity activity) { this.activity = activity; DependencyInjectionService.getInstance().inject(this); } public void processUpdates() { JSONArray updates = checkForUpdates(); if(updates == null || updates.length() == 0) return; MessageTuple message = buildUpdateMessage(updates); if(message == null || message.message.length() == 0) return; displayUpdateDialog(message); } public static class MessageTuple { public String message = null; public List<String> linkText = new ArrayList<String>(); public List<OnClickListener> click = new ArrayList<OnClickListener>(); } private static interface DialogShower { void showDialog(Activity activity); } private void tryShowDialog(DialogShower ds) { try { ds.showDialog(activity); } catch (BadTokenException bt) { try { Activity current = (Activity) ContextManager.getContext(); ds.showDialog(current); } catch (ClassCastException c) { // Oh well, context wasn't an activity } catch (BadTokenException bt2) { // Oh well, activity isn't running } } } protected void displayUpdateDialog(final MessageTuple message) { if(activity == null) return; if (message.linkText.size() > 0) { final DialogShower ds = new DialogShower() { @Override public void showDialog(Activity a) { try { final Dialog d = new Dialog(activity, R.style.ReminderDialog); d.setContentView(R.layout.update_message_view); // TODO: Make HTML message WebView messageView = (WebView) d.findViewById(R.id.reminder_message); String html = "<html><body>" + message.message + "</body></html>"; messageView.loadDataWithBaseURL("file:///android_asset/", html, "text/html", "utf-8", null); messageView.setBackgroundColor(0); d.findViewById(R.id.dismiss).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { d.dismiss(); } }); LinearLayout root = (LinearLayout) d.findViewById(R.id.reminder_root); DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); TypedValue themeColor = new TypedValue(); activity.getTheme().resolveAttribute(R.attr.asThemeTextColor, themeColor, false); int color = activity.getResources().getColor(themeColor.data); for (int i = 0; i < message.linkText.size(); i++) { Button linkButton = new Button(activity); linkButton.setText(message.linkText.get(i)); final OnClickListener click = message.click.get(i); linkButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { click.onClick(v); d.dismiss(); } }); LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, (int) (metrics.density * 35)); params.leftMargin = params.rightMargin = (int) (metrics.density * 5); params.bottomMargin = (int) (metrics.density * 10); linkButton.setTextColor(Color.WHITE); linkButton.setTextSize(20); linkButton.setBackgroundColor(color); linkButton.setLayoutParams(params); linkButton.setPadding(0, 0, 0, 0); root.addView(linkButton); } d.show(); } catch (Exception e) { // This should never ever crash } } }; activity.runOnUiThread(new Runnable() { @Override public void run() { tryShowDialog(ds); } }); } } protected MessageTuple buildUpdateMessage(JSONArray updates) { for(int i = 0; i < updates.length(); i++) { JSONObject update; try { update = updates.getJSONObject(i); } catch (JSONException e) { continue; } String date = update.optString("date", null); String message = update.optString("message", null); String plugin = update.optString("plugin", null); String notPlugin = update.optString("notplugin", null); if(message == null) continue; if(plugin != null) { if(!pluginConditionMatches(plugin)) continue; } if(notPlugin != null) { if(pluginConditionMatches(notPlugin)) continue; } MessageTuple toReturn = new MessageTuple(); toReturn.message = message; String type = update.optString("type", null); if ("screen".equals(type) || "pref".equals(type)) { String linkText = update.optString("link"); OnClickListener click = getClickListenerForUpdate(update, type); if (click == null) continue; toReturn.linkText.add(linkText); toReturn.click.add(click); } else { JSONArray links = update.optJSONArray("links"); if (links != null) { for (int j = 0; j < links.length(); j++) { JSONObject link = links.optJSONObject(j); if (link == null) continue; String linkText = link.optString("title"); if (TextUtils.isEmpty(linkText)) continue; final String url = link.optString("url"); OnClickListener click = new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); activity.startActivity(intent); } }; toReturn.linkText.add(linkText); toReturn.click.add(click); } } } if(messageAlreadySeen(date, message)) continue; return toReturn; } return null; } private OnClickListener getClickListenerForUpdate(JSONObject update, String type) { if ("pref".equals(type)) { try { if (!update.has("action_list")) return null; JSONArray prefSpec = update.getJSONArray("action_list"); if (prefSpec.length() == 0) return null; final String prefArray = prefSpec.toString(); return new View.OnClickListener() { @Override public void onClick(View b) { Intent prefScreen = new Intent(activity, UpdateMessagePreference.class); prefScreen.putExtra(UpdateMessagePreference.TOKEN_PREFS_ARRAY, prefArray); activity.startActivityForResult(prefScreen, 0); } }; } catch (JSONException e) { return null; } } else if ("screen".equals(type)) { try { if (!update.has("action_list")) return null; JSONArray screens = update.getJSONArray("action_list"); if (screens.length() == 0) return null; final ArrayList<String> screenList = new ArrayList<String>(); for (int i = 0; i < screens.length(); i++) { String screen = screens.getString(i).trim(); if (!TextUtils.isEmpty(screen)) screenList.add(screen); } return new View.OnClickListener() { @Override public void onClick(View v) { Intent screenFlow = new Intent(activity, UpdateScreenFlow.class); screenFlow.putStringArrayListExtra(UpdateScreenFlow.TOKEN_SCREENS, screenList); activity.startActivity(screenFlow); } }; } catch (JSONException e) { return null; } } return null; } private boolean pluginConditionMatches(String plugin) { // handle internal plugin specially if(PLUGIN_GTASKS.equals(plugin)) return gtasksPreferenceService.isLoggedIn(); else return addOnService.isInstalled(plugin); } private boolean messageAlreadySeen(String date, String message) { if(date != null) message = date + message; String hash = AndroidUtilities.md5(message); TodorooCursor<StoreObject> cursor = storeObjectDao.query(Query.select(StoreObject.ID). where(StoreObjectCriteria.byTypeAndItem(UpdateMessage.TYPE, hash))); try { if(cursor.getCount() > 0) return true; } finally { cursor.close(); } StoreObject newUpdateMessage = new StoreObject(); newUpdateMessage.setValue(StoreObject.TYPE, UpdateMessage.TYPE); newUpdateMessage.setValue(UpdateMessage.HASH, hash); storeObjectDao.persist(newUpdateMessage); return false; } private JSONArray checkForUpdates() { PackageManager pm = ContextManager.getContext().getPackageManager(); try { PackageInfo pi = pm.getPackageInfo(Constants.PACKAGE, PackageManager.GET_META_DATA); int versionCode = pi.versionCode; String url = URL + "?version=" + versionCode + "&" + "language=" + Locale.getDefault().getISO3Language() + "&" + "market=" + Constants.MARKET_STRATEGY.strategyId() + "&" + "actfm=" + (actFmPreferenceService.isLoggedIn() ? "1" : "0") + "&" + "premium=" + (ActFmPreferenceService.isPremiumUser() ? "1" : "0"); String result = restClient.get(url); //$NON-NLS-1$ if(TextUtils.isEmpty(result)) return null; return new JSONArray(result); } catch (IOException e) { return null; } catch (NameNotFoundException e) { return null; } catch (JSONException e) { return null; } } /** store object for messages a user has seen */ static class UpdateMessage { /** type*/ public static final String TYPE = "update-message"; //$NON-NLS-1$ /** message contents */ public static final StringProperty HASH = new StringProperty(StoreObject.TABLE, StoreObject.ITEM.name); } }