/*
* Copyright 2012 The Stanford MobiSocial Laboratory
*
* 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 mobisocial.musubi.service;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import mobisocial.musubi.App;
import mobisocial.musubi.model.MApp;
import mobisocial.musubi.model.helpers.AppManager;
import mobisocial.musubi.util.Util;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.javatuples.Pair;
import org.json.JSONArray;
import org.json.JSONObject;
import org.mobisocial.corral.ContentCorral;
import android.app.Service;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.util.Log;
public class AppUpdaterService extends Service {
public static boolean DBG = false;
public static final String TAG = AppUpdaterService.class.getName();
HandlerThread mThread;
AppManager mAppManager;
SQLiteOpenHelper mDatabaseSource;
Handler mUpdateHandler;
public AppUpdaterService() {
super();
}
class UpdateApps extends ContentObserver {
final Handler mHandler;
public UpdateApps(Handler handler) {
super(handler);
mHandler = handler;
}
@Override
public void onChange(boolean selfChange) {
DefaultHttpClient hc = new DefaultHttpClient();
SQLiteDatabase db = mDatabaseSource.getWritableDatabase();
long[] apps = mAppManager.listApps();
for(long app_id : apps) {
try {
MApp app = mAppManager.lookupApp(app_id);
//just in case someone deletes it
if(app == null) {
continue;
}
//no manifests for native apps for now
if(app.webAppUrl_ == null)
continue;
String name = null;
String web_url = null;
List<Pair<String, String>> type_action_list = new LinkedList<Pair<String,String>>();
URL url = new URL(app.webAppUrl_);
URL[] possible_urls = null;
if(url.getQuery() == null) {
possible_urls = new URL[2];
//try appending /config.json
String no_slash = app.webAppUrl_;
if(no_slash.charAt(no_slash.length() - 1) == '/')
no_slash = no_slash.substring(0, no_slash.length() - 1);
possible_urls[0] = new URL(no_slash + "/config.json");
//then try trimming
String parent = app.webAppUrl_.substring(0, app.webAppUrl_.lastIndexOf('/'));
possible_urls[1] = new URL(parent + "/config.json");
} else {
possible_urls = new URL[1];
//try triming
String parent = app.webAppUrl_.substring(0, app.webAppUrl_.lastIndexOf('/'));
possible_urls[0] = new URL(parent + "/config.json");
}
for(URL config_url : possible_urls) {
HttpResponse res;
try {
HttpGet hg = new HttpGet(config_url.toString());
res = hc.execute(hg);
if(res == null) {
throw new Exception("HTTP no result");
}
StatusLine sl = res.getStatusLine();
if(sl == null) {
throw new Exception("HTTP never completed");
} else if(sl.getStatusCode() < 200 || sl.getStatusCode() >= 400) {
String body = "<no response>";
try {
HttpEntity he = res.getEntity();
if(he != null) {
InputStream in = he.getContent();
if(in != null) {
body = IOUtils.toString(in);
}
}
} catch(IOException e) {}
throw new Exception("HTTP returned " + sl.toString() + ":\n" + body);
}
HttpEntity he = res.getEntity();
String manifest_string = IOUtils.toString(he.getContent());
JSONObject manifest = new JSONObject(manifest_string);
name = manifest.optString("name");
if(name.equals(""))
name = null;
web_url = manifest.optString("web_url");
if(web_url.equals(""))
web_url = null;
JSONObject obj_actions = manifest.optJSONObject("obj_actions");
if(obj_actions != null) {
for(Iterator type_it = obj_actions.keys(); type_it.hasNext();) {
String type = (String)type_it.next();
JSONArray array = obj_actions.getJSONArray(type);
for(int i = 0; i < array.length(); ++i) {
type_action_list.add(Pair.with(type, array.getString(i)));
}
}
}
//if we already fetched it, don't try the alternative urls
if (DBG) Log.i(TAG, "fetch config json file from for app " + app_id);
break;
} catch (Exception e) {
//TODO: reschedule fetch sometime?
if (DBG) Log.e(TAG, "unable to fetch config json file from for app " + app_id, e);
continue;
}
}
if (web_url != null) {
app.webAppUrl_ = web_url;
ContentCorral.cacheWebApp(Uri.parse(web_url));
}
long startTime = System.currentTimeMillis();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
db.beginTransactionNonExclusive();
} else {
db.beginTransaction();
}
try {
//double check the delete condition
app = mAppManager.lookupApp(app_id);
if(app == null) {
continue;
}
mAppManager.deleteAppActionWithForApp(app);
if(name != null)
app.name_ = name;
if(web_url != null) {
Log.w(TAG, "hmm, trying to change app url... from " + app.webAppUrl_ + " to " + web_url);
app.webAppUrl_ = web_url;
}
//TODO: other info? like icon... would have had to be fetched above outside this body
mAppManager.updateApp(app);
for(Pair<String, String> type_action : type_action_list) {
String type = type_action.getValue0();
String action = type_action.getValue1();
mAppManager.insertAppAction(app, type, action);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
long totalTime = System.currentTimeMillis() - startTime;
Log.d(TAG, "++++ AppManifest transaction took " + totalTime + "ms.");
} catch(Throwable t) {
Log.e(TAG, "failed to update app " + app_id, t);
}
}
}
}
@Override
public void onCreate() {
mDatabaseSource = App.getDatabaseSource(this);
mThread = new HandlerThread("AppManifests");
mThread.setPriority(Thread.MIN_PRIORITY);
mThread.start();
mAppManager = new AppManager(mDatabaseSource);
mUpdateHandler = new Handler(mThread.getLooper());
ContentResolver resolver = getContentResolver();
resolver.registerContentObserver(MusubiService.UPDATE_APP_MANIFESTS, false,
new UpdateApps(mUpdateHandler));
//kick it off once per boot
resolver.notifyChange(MusubiService.UPDATE_APP_MANIFESTS, null);
Log.w(TAG, "service is now running");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "Received start id " + startId + ": " + intent);
return START_STICKY;
}
@Override
public void onDestroy() {
//kick the background thread into shutdown mode
mUpdateHandler.post(new Runnable() {
@Override
public void run() {
mThread.getLooper().quit();
}
});
//wait for it to clean up
try {
mThread.join();
} catch (InterruptedException e) {}
}
public class AppUpdateServiceBinder extends Binder {
public AppUpdaterService getService() {
return AppUpdaterService.this;
}
}
private final IBinder mBinder = new AppUpdateServiceBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}