package com.transistorsoft.rnbackgroundgeolocation;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeArray;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.modules.core.RCTNativeAppEventEmitter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.google.android.gms.common.GoogleApiAvailability;
import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation;
import com.transistorsoft.locationmanager.adapter.TSCallback;
import com.transistorsoft.locationmanager.settings.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
/**
* Created by chris on 2015-10-30.
*/
public class RNBackgroundGeolocationModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener {
private static final String TAG = "TSLocationManager";
public static final String ACCESS_COARSE_LOCATION = Manifest.permission.ACCESS_COARSE_LOCATION;
public static final String ACCESS_FINE_LOCATION = Manifest.permission.ACCESS_FINE_LOCATION;
public static final int REQUEST_ACTION_START = 1;
public static final int REQUEST_ACTION_GET_CURRENT_POSITION = 2;
public static final int REQUEST_ACTION_START_GEOFENCES = 3;
private boolean initialized = false;
private boolean configured = false;
private Intent launchIntent;
private static final String EVENT_WATCHPOSITION = "watchposition";
private HashMap<String, Callback> startCallback;
public RNBackgroundGeolocationModule(ReactApplicationContext reactContext) {
super(reactContext);
reactContext.addLifecycleEventListener(this);
}
@Override
public void initialize() {
// do nothing
}
@Override
public String getName() {
return "RNBackgroundGeolocation";
}
@Override
public void onHostResume() {
if (!initialized) {
initializeLocationManager();
}
}
@Override
public void onHostPause() {
}
@Override
public void onHostDestroy() {
initialized = false;
configured = false;
getAdapter().onActivityDestroy();
}
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
}
@Override
public void onNewIntent(Intent intent) {
}
@ReactMethod
public void configure(ReadableMap config, final Callback success, final Callback failure) {
if (configured) { return; }
configured = true;
BackgroundGeolocation adapter = getAdapter();
adapter.on(BackgroundGeolocation.EVENT_LOCATION, (new TSCallback() {
@Override
public void success(Object o) {
onLocationChange((JSONObject) o);
}
@Override
public void error(Object o) {
onLocationError((Integer) o);
}
}));
adapter.on(BackgroundGeolocation.EVENT_MOTIONCHANGE, (new TSCallback() {
@Override
public void success(Object o) {
onMotionChange((JSONObject) o);
}
@Override
public void error(Object o) {
}
}));
adapter.on(BackgroundGeolocation.EVENT_ACTIVITYCHANGE, (new TSCallback() {
@Override
public void success(Object o) {
onActivityChange((String) o);
}
@Override
public void error(Object o) {
}
}));
adapter.on(BackgroundGeolocation.EVENT_HTTP, (new TSCallback() {
@Override
public void success(Object o) {
onHttpResponse((JSONObject) o);
}
@Override
public void error(Object o) {
onHttpResponse((JSONObject) o);
}
}));
adapter.on(BackgroundGeolocation.EVENT_HEARTBEAT, (new TSCallback() {
@Override
public void success(Object o) {
onHeartbeat((JSONObject) o);
}
@Override
public void error(Object o) {
}
}));
adapter.on(BackgroundGeolocation.EVENT_GEOFENCESCHANGE, (new TSCallback() {
@Override
public void success(Object o) {
onGeofencesChange((JSONObject) o);
}
@Override
public void error(Object o) {
}
}));
adapter.on(BackgroundGeolocation.EVENT_GEOFENCE, (new TSCallback() {
@Override
public void success(Object o) {
onGeofence((JSONObject) o);
}
@Override
public void error(Object o) {
}
}));
adapter.on(BackgroundGeolocation.EVENT_SCHEDULE, (new TSCallback() {
@Override
public void success(Object o) {
onSchedule((JSONObject) o);
}
@Override
public void error(Object o) {
}
}));
adapter.on(BackgroundGeolocation.EVENT_PLAY_SERVICES_CONNECT_ERROR, (new TSCallback() {
@Override
public void success(Object o) {
onPlayServicesConnectError((Integer)o);
}
@Override
public void error(Object o) {
}
}));
adapter.on(BackgroundGeolocation.EVENT_PROVIDERCHANGE, (new TSCallback() {
@Override
public void success(Object provider) { onProviderChange((JSONObject) provider); }
@Override
public void error(Object o) {}
}));
TSCallback callback = new TSCallback() {
public void success(Object state) {
success.invoke(getState());
}
public void error(Object error) {
failure.invoke("Unknown error");
}
};
adapter.configure(mapToJson(config), callback);
}
@ReactMethod
public void start(Callback success, Callback failure) {
if (startCallback != null) {
failure.invoke("Waiting for a previous start action to complete");
return;
}
startCallback = new HashMap<>();
startCallback.put("success", success);
startCallback.put("failure", failure);
if (hasPermission(ACCESS_COARSE_LOCATION) && hasPermission(ACCESS_FINE_LOCATION)) {
setEnabled(true);
} else {
String[] permissions = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION};
requestPermissions(REQUEST_ACTION_START, permissions);
}
}
@ReactMethod
public void startSchedule(Callback success, Callback failure) {
if (getAdapter().startSchedule()) {
success.invoke(getState());
} else {
failure.invoke("Failed to start schedule. Did you configure a #schedule?");
}
}
@ReactMethod
public void stopSchedule(Callback success, Callback failure) {
getAdapter().stopSchedule();
success.invoke(getState());
}
@ReactMethod
public void startGeofences(Callback success, Callback failure) {
if (hasPermission(ACCESS_COARSE_LOCATION) && hasPermission(ACCESS_FINE_LOCATION)) {
getAdapter().startGeofences(new StartGeofencesCallback(success, failure));
} else {
startCallback = new HashMap<>();
startCallback.put("success", success);
startCallback.put("failure", failure);
String[] permissions = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION};
requestPermissions(REQUEST_ACTION_START_GEOFENCES, permissions);
}
}
private class StartGeofencesCallback implements TSCallback {
private Callback mSuccess;
private Callback mFailure;
public StartGeofencesCallback(Callback success, Callback failure) {
mSuccess = success;
mFailure = failure;
}
@Override
public void success(Object state) {
mSuccess.invoke(getState());
}
@Override
public void error(Object error) {
mFailure.invoke((String) error);
}
}
@ReactMethod
public void stop(final Callback success, final Callback failure) {
startCallback = null;
TSCallback callback = new TSCallback() {
@Override
public void success(Object state) {
try {
success.invoke(jsonToMap((JSONObject)state));
} catch (JSONException e) {
success.invoke(getState());
}
}
@Override
public void error(Object error) {
failure.invoke((String) error);
}
};
getAdapter().stop(callback);
}
@ReactMethod
public void changePace(Boolean moving, final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object result) {
success.invoke(getState());
}
public void error(Object result) {
failure.invoke(result);
}
};
getAdapter().changePace(moving, callback);
}
@ReactMethod
public void setConfig(ReadableMap config, final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
@Override
public void success(Object o) {
success.invoke(getState());
}
@Override
public void error(Object o) {
failure.invoke("Unknown error");
}
};
getAdapter().setConfig(mapToJson(config), callback);
}
@ReactMethod
public void getState(Callback success, Callback failure) {
WritableMap state = getState();
if (state != null && !state.hasKey("error")) {
success.invoke(state);
} else {
failure.invoke(state);
}
}
private WritableMap getState() {
try {
return jsonToMap(Settings.getState());
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
@ReactMethod
public void getLocations(final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object result) {
try {
success.invoke(convertJsonToArray((JSONArray) result));
} catch (JSONException e) {
e.printStackTrace();
failure.invoke(e.getMessage());
}
}
public void error(Object error) {
failure.invoke((String) error);
}
};
getAdapter().getLocations(callback);
}
@ReactMethod
public void getCount(final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object result) {
success.invoke((int) result);
}
public void error(Object error) {
failure.invoke((String) error);
}
};
getAdapter().getCount(callback);
}
@ReactMethod
public void insertLocation(ReadableMap params, final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object result) {
success.invoke((String)result);
}
public void error(Object error) {
failure.invoke((String)error);
}
};
getAdapter().insertLocation(mapToJson(params), callback);
}
// @deprecated -> #destroyLocations
@ReactMethod
public void clearDatabase(Callback success, Callback failure) {
destroyLocations(success, failure);
}
@ReactMethod
public void destroyLocations(final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object result) {
success.invoke((Boolean) result);
}
public void error(Object error) {
failure.invoke((String)error);
}
};
getAdapter().destroyLocations(callback);
}
@ReactMethod
public void destroyLog(final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object log) {
success.invoke((String) log);
}
public void error(Object error) {
failure.invoke((String)error);
}
};
getAdapter().destroyLog(callback);
}
private class DestroyLogCallback implements TSCallback {
private Callback mSuccess;
private Callback mFailure;
public DestroyLogCallback(Callback success, Callback failure) {
mSuccess = success;
mFailure = failure;
}
@Override
public void success(Object result) {
mSuccess.invoke((String) result);
}
@Override
public void error(Object error) {
mFailure.invoke((String) error);
}
}
@ReactMethod
public void sync(final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object result) {
try {
success.invoke(convertJsonToArray((JSONArray) result));
} catch (JSONException e) {
failure.invoke(e.getMessage());
}
}
public void error(Object error) {
failure.invoke((String)error);
}
};
getAdapter().sync(callback);
}
@ReactMethod
public void getCurrentPosition(ReadableMap options, final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object location) {
try {
success.invoke(jsonToMap((JSONObject) location));
} catch (JSONException e) {
failure.invoke(e.getMessage());
}
}
public void error(Object error) {
failure.invoke(error);
}
};
getAdapter().getCurrentPosition(mapToJson(options), callback);
}
@ReactMethod
public void watchPosition(ReadableMap options, final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object location) {
try {
sendEvent(EVENT_WATCHPOSITION, jsonToMap((JSONObject)location));
} catch (JSONException e) {
e.printStackTrace();
}
}
public void error(Object error) {
failure.invoke(error);
}
};
getAdapter().watchPosition(mapToJson(options), callback);
success.invoke();
}
@ReactMethod
public void stopWatchPosition(final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object result) {
success.invoke();
}
public void error(Object error) {
failure.invoke(error);
}
};
getAdapter().stopWatchPosition(callback);
}
@ReactMethod
public void getOdometer(Callback success, Callback failure) {
success.invoke(getAdapter().getOdometer());
}
@ReactMethod
public void setOdometer(Float value, final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object location) {
try {
success.invoke(jsonToMap((JSONObject) location));
} catch (JSONException e) {
failure.invoke(e.getMessage());
}
}
public void error(Object errorCode) {
failure.invoke((Integer) errorCode);
}
};
getAdapter().setOdometer(value, callback);
}
@ReactMethod
public void addGeofence(ReadableMap options, final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
@Override
public void success(Object o) {
success.invoke((String) o);
}
@Override
public void error(Object o) {
failure.invoke((String) o);
}
};
getAdapter().addGeofence(mapToJson(options), callback);
}
@ReactMethod
public void addGeofences(ReadableArray geofences, final Callback success, final Callback failure) {
JSONArray json = new JSONArray();
for (int n=0;n<geofences.size();n++) {
json.put(mapToJson(geofences.getMap(n)));
}
TSCallback callback = new TSCallback() {
@Override
public void success(Object o) {
success.invoke((Boolean) o);
}
@Override
public void error(Object o) {
failure.invoke((String) o);
}
};
getAdapter().addGeofences(json, callback);
}
@ReactMethod
public void removeGeofence(String identifier, final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
@Override
public void success(Object identifier) {
success.invoke((String) identifier);
}
@Override
public void error(Object o) {
failure.invoke((String) o);
}
};
getAdapter().removeGeofence(identifier, callback);
}
@ReactMethod
public void removeGeofences(final Callback success, final Callback failure) {
// TODO allow JS api to delete a list-of-geofences.
TSCallback callback = new TSCallback() {
@Override
public void success(Object o) {
success.invoke((Boolean) o);
}
@Override
public void error(Object o) {
failure.invoke((String) o);
}
};
try {
// TODO accept WritableArray geofences from Client js API, allowing to remove a set of geofences
WritableArray geofences = new WritableNativeArray();
getAdapter().removeGeofences(arrayToJson(geofences),callback);
} catch (JSONException e) {
e.printStackTrace();
}
}
@ReactMethod
public void getGeofences(final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
@Override
public void success(Object geofences) {
try {
success.invoke(convertJsonToArray((JSONArray) geofences));
} catch (JSONException e) {
e.printStackTrace();
failure.invoke(e.getMessage());
}
}
@Override
public void error(Object error) {
}
};
getAdapter().getGeofences(callback);
}
/**
* Android doesn't support (or require) background-tasks. This method is here for compatibility with iOS API
*/
@ReactMethod
public void beginBackgroundTask(Callback success) {
Integer taskId = 0;
success.invoke(taskId);
}
/**
* Android doesn't support (or require) background-tasks. This method is here for compatibility with iOS API
*/
@ReactMethod
public void finish(Integer taskId) {
}
@ReactMethod
public void playSound( int soundId) {
getAdapter().startTone(soundId);
}
@ReactMethod
public void getLog(final Callback success, final Callback failure) {
TSCallback callback = new TSCallback() {
public void success(Object log) {
success.invoke((String) log);
}
public void error(Object error) {
failure.invoke((String) error);
}
};
getAdapter().getLog(callback);
}
@ReactMethod
public void emailLog(String email, Callback success, Callback error) {
this.getAdapter().getLog(new EmailLogCallback(email, success, error));
}
private class EmailLogCallback implements TSCallback {
private String mEmail;
private Callback mSuccess;
private Callback mError;
public EmailLogCallback(String email, Callback success, Callback error) {
mEmail = email;
mSuccess = success;
mError = error;
}
@Override
public void success(Object result) {
String log = (String) result;
Intent mailer = new Intent(Intent.ACTION_SEND);
mailer.setType("message/rfc822");
mailer.putExtra(Intent.EXTRA_EMAIL, new String[]{mEmail});
mailer.putExtra(Intent.EXTRA_SUBJECT, "BackgroundGeolocation log");
try {
JSONObject state = mapToJson(getState());
if (state.has("license")) {
state.put("license", "<SECRET>");
}
if (state.has("orderId")) {
state.put("orderId", "<SECRET>");
}
mailer.putExtra(Intent.EXTRA_TEXT, state.toString(2));
} catch (JSONException e) {
e.printStackTrace();
}
File file = new File(Environment.getExternalStorageDirectory(), "background-geolocation.log");
try {
FileOutputStream stream = new FileOutputStream(file);
try {
stream.write(log.getBytes());
stream.close();
mailer.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
file.deleteOnExit();
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
Log.i(TAG, "FileNotFound");
e.printStackTrace();
}
Activity activity = getCurrentActivity();
try {
activity.startActivityForResult(Intent.createChooser(mailer, "Send log: " + mEmail + "..."), 1);
mSuccess.invoke();
} catch (android.content.ActivityNotFoundException ex) {
Toast.makeText(activity, "There are no email clients installed.", Toast.LENGTH_SHORT).show();
mError.invoke("There are no email clients installed");
}
}
@Override
public void error(Object error) {
mError.invoke((String) error);
}
}
private void setEnabled(boolean value) {
Log.i(TAG, "- setEnabled: " + value + ", current value: " + Settings.getEnabled());
BackgroundGeolocation adapter = getAdapter();
if (value) {
TSCallback callback = new TSCallback() {
public void success(Object state) {
if (startCallback != null) {
Callback success = startCallback.get("success");
success.invoke(getState());
startCallback = null;
}
}
public void error(Object error) {
startCallback = null;
}
};
adapter.start(callback);
}
}
// Event-handlers
private void onLocationChange(JSONObject location) {
try {
sendEvent(BackgroundGeolocation.EVENT_LOCATION, jsonToMap(location));
} catch (JSONException e) {
e.printStackTrace();
}
}
private void onMotionChange(JSONObject params) {
try {
sendEvent(BackgroundGeolocation.EVENT_MOTIONCHANGE, jsonToMap(params));
} catch (JSONException e) {
e.printStackTrace();
}
}
public void onHttpResponse(JSONObject params) {
try {
sendEvent(BackgroundGeolocation.EVENT_HTTP, jsonToMap(params));
} catch (JSONException e) {
e.printStackTrace();
}
}
private void onHeartbeat(JSONObject params) {
try {
sendEvent(BackgroundGeolocation.EVENT_HEARTBEAT, jsonToMap(params));
} catch (JSONException e) {
e.printStackTrace();
}
}
private void onGeofence(JSONObject params) {
try {
sendEvent(BackgroundGeolocation.EVENT_GEOFENCE, jsonToMap(params));
} catch (JSONException e) {
e.printStackTrace();
}
}
private void onGeofencesChange(JSONObject params) {
try {
sendEvent(BackgroundGeolocation.EVENT_GEOFENCESCHANGE, jsonToMap(params));
} catch (JSONException e) {
e.printStackTrace();
}
}
private void onSchedule(JSONObject params) {
try {
sendEvent(BackgroundGeolocation.EVENT_SCHEDULE, jsonToMap(params));
} catch (JSONException e) {
e.printStackTrace();
}
}
private void onActivityChange(String activityName) {
sendEvent(BackgroundGeolocation.EVENT_ACTIVITYCHANGE, activityName);
}
private void onProviderChange(JSONObject provider) {
try {
sendEvent(BackgroundGeolocation.EVENT_PROVIDERCHANGE, jsonToMap(provider));
} catch(JSONException e) {
e.printStackTrace();
}
}
private void onLocationError(Integer code) {
WritableMap params = new WritableNativeMap();
params.putInt("code", code);
params.putString("type", "location");
sendEvent(BackgroundGeolocation.EVENT_ERROR, params);
}
private void onPlayServicesConnectError(Integer errorCode) {
Activity activity = getCurrentActivity();
if (activity == null) {
return;
}
GoogleApiAvailability.getInstance().getErrorDialog(getCurrentActivity(), errorCode, 1001).show();
}
private void sendEvent(String eventName, WritableMap params) {
getReactApplicationContext().getJSModule(RCTNativeAppEventEmitter.class).emit(eventName, params);
}
private void sendEvent(String eventName, String result) {
getReactApplicationContext().getJSModule(RCTNativeAppEventEmitter.class).emit(eventName, result);
}
public static WritableMap jsonToMap(JSONObject jsonObject) throws JSONException {
WritableMap map = new WritableNativeMap();
Iterator<String> iterator = jsonObject.keys();
while (iterator.hasNext()) {
String key = iterator.next();
Object value = jsonObject.get(key);
if (value instanceof JSONObject) {
map.putMap(key, jsonToMap((JSONObject) value));
} else if (value instanceof JSONArray) {
map.putArray(key, convertJsonToArray((JSONArray) value));
} else if (value instanceof Boolean) {
map.putBoolean(key, (Boolean) value);
} else if (value instanceof Integer) {
map.putInt(key, (Integer) value);
} else if (value instanceof Double) {
map.putDouble(key, (Double) value);
} else if (value instanceof String) {
map.putString(key, (String) value);
} else {
map.putString(key, value.toString());
}
}
return map;
}
public static WritableArray convertJsonToArray(JSONArray jsonArray) throws JSONException {
WritableArray array = new WritableNativeArray();
for (int i = 0; i < jsonArray.length(); i++) {
Object value = jsonArray.get(i);
if (value instanceof JSONObject) {
array.pushMap(jsonToMap((JSONObject) value));
} else if (value instanceof JSONArray) {
array.pushArray(convertJsonToArray((JSONArray) value));
} else if (value instanceof Boolean) {
array.pushBoolean((Boolean) value);
} else if (value instanceof Integer) {
array.pushInt((Integer) value);
} else if (value instanceof Double) {
array.pushDouble((Double) value);
} else if (value instanceof String) {
array.pushString((String) value);
} else {
array.pushString(value.toString());
}
}
return array;
}
public static JSONObject mapToJson(ReadableMap map) {
ReadableMapKeySetIterator iterator = map.keySetIterator();
JSONObject json = new JSONObject();
try {
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
switch (map.getType(key)) {
case String:
json.put(key, map.getString(key));
break;
case Boolean:
json.put(key, map.getBoolean(key));
break;
case Number:
json.put(key, map.getDouble(key));
break;
case Map:
json.put(key, mapToJson(map.getMap(key)));
break;
case Array:
json.put(key, arrayToJson(map.getArray(key)));
break;
}
}
} catch (JSONException e) {
e.printStackTrace();
}
return json;
}
public static JSONArray arrayToJson(ReadableArray readableArray) throws JSONException {
JSONArray jsonArray = new JSONArray();
for(int i=0; i < readableArray.size(); i++) {
ReadableType valueType = readableArray.getType(i);
switch (valueType){
case Null:
jsonArray.put(JSONObject.NULL);
break;
case Boolean:
jsonArray.put(readableArray.getBoolean(i));
break;
case Number:
jsonArray.put(readableArray.getInt(i));
break;
case String:
jsonArray.put(readableArray.getString(i));
break;
case Map:
jsonArray.put(mapToJson(readableArray.getMap(i)));
break;
case Array:
jsonArray.put(arrayToJson(readableArray.getArray(i)));
break;
}
}
return jsonArray;
}
// TODO placehold for implementing Android M permissions request. Just return true for now.
private Boolean hasPermission(String permission) {
return true;
}
// TODO placehold for implementing Android M permissions request. Just return true for now.
private void requestPermissions(int requestCode, String[] action) {
}
private void initializeLocationManager() {
Activity activity = getCurrentActivity();
if (activity == null) {
return;
}
launchIntent = activity.getIntent();
if (launchIntent.hasExtra("forceReload")) {
activity.moveTaskToBack(true);
}
getAdapter();
initialized = true;
}
private BackgroundGeolocation getAdapter() {
return BackgroundGeolocation.getInstance(getReactApplicationContext(), launchIntent);
}
@Override
public void onCatalystInstanceDestroy() {
initialized = false;
getAdapter().onActivityDestroy();
}
}