public class Aikuma extends android.app.Application {

	private static Aikuma instance;
	private static List<Language> languages;
	private static Map<String, String> languageCodeMap;
	private static SharedPreferences preferences;

	/**
	 * The constructor.
	 */
	public Aikuma() {
		instance = this;
	}

	/**
	 * Static method that provides a context when needed by code not bound to
	 * any meaningful context.
	 *
	 * @return	A Context
	 */
	public static Context getContext() {
		return instance;
	}

	/**
	 * Gets the phone model name
	 * 
	 * @return	The device name (manufacturer + model)
	 */
	public static String getDeviceName() {
		return Build.MANUFACTURER + "-" + Build.MODEL;
	}
	
	/**
	 * Gets the android ID of the phone.
	 *
	 * @return	The android ID as a String.
	 */
	public static String getAndroidID() {
		return Secure.getString(
				getContext().getContentResolver(), Secure.ANDROID_ID);
	}

	/**
     * Checks whether the device is currently connected to a network
     * @return	boolean for status
     */
    public static boolean isDeviceOnline() {
    	if(preferences == null) {
    		preferences = 
    				PreferenceManager.getDefaultSharedPreferences(getContext());
    		AikumaSettings.isOnlyWifi = 
    				preferences.getBoolean(AikumaSettings.WIFI_MODE_KEY, true);
    	}
    	
    	ConnectivityManager connMgr = (ConnectivityManager)
                getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
    	NetworkInfo networkInfo;
    	if(AikumaSettings.isNetwork){
    		networkInfo = connMgr.getActiveNetworkInfo();   
    	} else if(AikumaSettings.isOnlyWifi) {
    		networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
    	} else {
    		networkInfo = null;
    	}
    	
    	if (networkInfo != null && networkInfo.isConnected()) {
            return true;
        }
    	return false;
    }
    
    /**
     * Checks if Wifi is enabled
     * @return	true if Wifi is enabled
     */
    public static boolean isWifiEnabled() {
    	WifiManager wifiMgr = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
    	return wifiMgr.isWifiEnabled();
    }
    
    /**
     * Check if the file is already archived/in the progress of archiving or not
     * 
     * @param emailAccount	Owner account of the file
     * @param fm			The file's model
     * @return				boolean of the archive status
     */
    public static boolean isArchived(String emailAccount, FileModel fm) {
    	//TODO: needs to be changed to make a query to fusionindex (because, the file could be archived in the other device)
    	
    	String itemCloudId = fm.getCloudIdentifier(0);
    	
    	Set<String> approvedSet;
    	Set<String> archivedSet;
    	int archiveProgress = -1;
    	
    	SharedPreferences privatePreferences = 
    			getContext().getSharedPreferences(emailAccount, MODE_PRIVATE);
    	String fileType = fm.getFileType();
    	if(fileType.equals("source") || fileType.equals("respeaking")) {
    		approvedSet = (HashSet<String>) privatePreferences.getStringSet(
    				AikumaSettings.APPROVED_RECORDING_KEY, new HashSet<String>());
    		archivedSet = (HashSet<String>) privatePreferences.getStringSet(
    				AikumaSettings.ARCHIVED_RECORDING_KEY, new HashSet<String>());
		} else if(fileType.equals("speaker")) {
			approvedSet = (HashSet<String>) privatePreferences.getStringSet(
    				AikumaSettings.APPROVED_SPEAKERS_KEY, new HashSet<String>());
			archivedSet = (HashSet<String>) privatePreferences.getStringSet(
					AikumaSettings.ARCHIVED_SPEAKERS_KEY, new HashSet<String>());
		} else {
			approvedSet = (HashSet<String>) privatePreferences.getStringSet(
    				AikumaSettings.APPROVED_OTHERS_KEY, new HashSet<String>());
			archivedSet = (HashSet<String>) privatePreferences.getStringSet(
					AikumaSettings.ARCHIVED_OTHERS_KEY, new HashSet<String>());
		}
    	
    	if(approvedSet.contains(itemCloudId)) {
    		String[] requestArchiveState = 
					privatePreferences.getString(itemCloudId, "").split("\\|");
			archiveProgress = 
					Integer.parseInt(requestArchiveState[1]);
    	}

    	return archivedSet.contains(itemCloudId) || (archiveProgress >= 0 && archiveProgress <= 3);
    }
    
    /**
	 * Sync the device with Google-Cloud
	 * 
	 * @param	activity	The activity which triggers sync
	 * @param	forceSync	Force the cloud-sync
	 */
	public static void syncRefresh(Activity activity, boolean forceSync) {
		if(forceSync) {
			if(!Aikuma.isDeviceOnline()) {
				Aikuma.showAlertDialog(activity, "Network needs to be connected");
				return;
			} else if(AikumaSettings.getCurrentUserToken() == null) {
				Aikuma.showAlertDialog(activity, 
						"You need to connect to Google-Drive with your account");
				return;
			} else {
				Intent syncIntent = new Intent(activity, GoogleCloudService.class);
				syncIntent.putExtra(GoogleCloudService.ACTION_KEY, "sync");
				syncIntent.putExtra(GoogleCloudService.ACCOUNT_KEY, 
						AikumaSettings.getCurrentUserId());
				syncIntent.putExtra(GoogleCloudService.TOKEN_KEY, 
						AikumaSettings.getCurrentUserToken());
				syncIntent.putExtra("forceSync", forceSync);
				activity.startService(syncIntent);
			}
		} else {
			SharedPreferences settings = activity.getSharedPreferences(
					AikumaSettings.getCurrentUserId(), MODE_PRIVATE);
	
			int sz = 0;
			sz += settings.getStringSet(AikumaSettings.APPROVED_RECORDING_KEY, 
					new HashSet<String>()).size();
			sz += settings.getStringSet(AikumaSettings.APPROVED_SPEAKERS_KEY,
					new HashSet<String>()).size();
			sz += settings.getStringSet(AikumaSettings.APPROVED_OTHERS_KEY,
					new HashSet<String>()).size();
			sz += settings.getStringSet(AikumaSettings.DOWNLOAD_RECORDING_KEY, 
					new HashSet<String>()).size();
			sz += settings.getStringSet(AikumaSettings.DOWNLOAD_SPEAKERS_KEY,
					new HashSet<String>()).size();
			sz += settings.getStringSet(AikumaSettings.DOWNLOAD_OTHERS_KEY,
					new HashSet<String>()).size();

			if(sz > 0) {
	    		// If there are items to be uploaded,
				// start the GoogleCloud upload service 
	    		Intent intent = new Intent(activity, 
	    				GoogleCloudService.class);
	    		intent.putExtra(GoogleCloudService.ACTION_KEY, "retry");
	    		intent.putExtra(GoogleCloudService.ACCOUNT_KEY, 
						AikumaSettings.getCurrentUserId());
				intent.putExtra(GoogleCloudService.TOKEN_KEY, 
						AikumaSettings.getCurrentUserToken());
				activity.startService(intent);
			}
		}
	}
    
    
    /**
     * Return an arraylist of available google-accounts in a device
     * @return	ArrayList of google-accounts
     */
    public static ArrayList<String> getGoogleAccounts() {
    	ArrayList<String> accountList = new ArrayList<String>();
    	
    	Account[] accounts = 
    			AccountManager.get(getContext()).getAccountsByType("com.google");
    	for(Account ac : accounts) {
    		accountList.add(ac.name);
    	}
    	
    	return accountList;
    }
    
    
    /**
     * Show the warning dialog with the message
     * @param activity	The activity where the message will be shown
     * @param message	the message shown in the dialog
     */
    public static void showAlertDialog(Context activity, String message) {
    	new AlertDialog.Builder(activity).setMessage(message).show();
    }
    
    /**
     * Show the confirmation dialog with the message
     * @param activity	The activity where the message will be shown
     * @param message	the message shown in the dialog
     * @param okListener	The callback interface when ok-button is pressed
     */
    public static void showConfirmationDialog(Context activity, String message, 
    		DialogInterface.OnClickListener okListener) {
    	new AlertDialog.Builder(activity)
    		.setMessage(message)
    		.setPositiveButton("OK", okListener)
    		.setNegativeButton("Cancel", null)
    		.show();
    }
	
	/**
	 * Returns the ISO 639-3 languages once they are loaded.
	 * 
	 * @return	the languages
	 */
	public static List<Language> getLanguages() {
		if (languages == null) {
			loadLanguages();
			while (languages == null) {
				//Wait patiently.
			}
		}
		return languages;
	}
	
	/**
	 * Returns the ISO 639-3 language map (code - name)
	 * 
	 * @return	the languageCodeMap
	 */
	public static Map<String, String> getLanguageCodeMap() {
		if (languageCodeMap == null) {
			loadLanguages();
			while (languageCodeMap == null) {
				//Wait patiently
			}
		}
		return languageCodeMap;
	}
	
	

	/**
	 * Loads the ISO 639-3 languages.
	 */
	public static void loadLanguages() {
		if (languages == null) {
			languages = new ArrayList<Language>();
			languageCodeMap = new HashMap<String, String>();
			
			if (loadLangCodesThread == null || !loadLangCodesThread.isAlive()) {
				loadLangCodesThread = new Thread(new Runnable() {
					public void run() {
						try {
							FileIO.readLangCodes(getContext().getResources(), 
									languages, languageCodeMap);
						} catch (IOException e) {
							// This should never happen.
							throw new RuntimeException("Cannot load languages");
						}
					}
				});
				loadLangCodesThread.start();
			}
		}
	}

	/**
	 * The thread used to load the language codes without interrupting the
	 * main thread.
	 */
	public static Thread loadLangCodesThread;
} public class Language implements Parcelable, Comparable<Language> {

	/**
	 * The name of the language
	 */
	private String name;

	/**
	 * The language's ISO 639-3 language code
	 */
	private String code;

	private void setName(String name) {
		this.name = name;
	}

	private void setCode(String code) {
		this.code = code;
	}

	public String getName() {
		return this.name;
	}

	/**
	 * Return the string of language code 
	 * (ISO639: three characters, Custom: first three chars)
	 * 
	 * @return	the language code
	 */
	public String getCode() {
		if(this.code.isEmpty() && !this.name.isEmpty()) {	// Custom language
			int end = this.name.length() > 3 ? 3 : this.name.length();
			return this.name.substring(0, end);
		}
		return this.code;
	}
	
	/**
	 * The minimal constructor.
	 *
	 * @param	name	The name of the language.
	 * @param	code	The ISO 639-3 code of the language.
	 */
	public Language(String name, String code) {
		setName(name);
		setCode(code);
	}

	/**
	 * Returns a string representation of the language
	 *
	 * @return A string instance.
	 */
	public String toString() {
		return getName() + " : " + getCode();
	}
	
	/**
	 * Returns a string used in the tag filename
	 * @return	A string of the tag contents
	 */
	public String toTagString() {
		if(this.code.isEmpty() && !this.name.isEmpty()) {	// Custom language
			return this.name;
		} else {	
			return "iso639_" + this.code;
		}
	}

	/**
	 * Describe the kinds of special objects contained in this Parcelable's
	 * marshalled representation.
	 *
	 * @return	an int (always zero).
	 */
	public int describeContents() {
		return 0;
	}

	/**
	 * Makes a parcel that represents this object
	 *
	 * @param	out	The parcel to be written to.
	 * @param	_flags	Additional flags about how the object should be
	 * written.
	 */
	public void writeToParcel(Parcel out, int _flags) {
		out.writeString(getName());
		out.writeString(this.code);
	}

	/**
	 * Generates an instance of a Language given a corresponding parcel.
	 */
	public static final Parcelable.Creator<Language> CREATOR = 
			new Parcelable.Creator<Language>() {
		public Language createFromParcel(Parcel in) {
			return new Language(in);
		}

		public Language[] newArray(int size) {
			return new Language[size];
		}
	};

	/**
	 * Constructor that takes a parcel as an argument so that the class can be
	 * parcelable.
	 *
	 * @param	in	A parcel containing the information on the Language to be
	 * constructed.
	 */
	public Language(Parcel in) {
		setName(in.readString());
		setCode(in.readString());
	}

	/**
	 * Encodes the Language object as a corresponding JSONObject.
	 *
	 * @return	A representation of the language as a JSONObject.
	 */
	public JSONObject encode() {
		JSONObject encodedLanguage = new JSONObject();
		encodedLanguage.put("name", getName());
		encodedLanguage.put("code", getCode());
		return encodedLanguage;
	}

	/**
	 * Encodes a list of languages as a corresponding JSONArray object.
	 *
	 * @param	languages	A list of languages to be encoded
	 * @return	A JSONArray object.
	 */
	public static JSONArray encodeList(List<Language> languages) {
		JSONArray languageArray = new JSONArray();
		for (Language language : languages) {
			//languageArray.add(language.encode());
			languageArray.add(language.getCode());
		}
		return languageArray;
	}

	/**
	 * Decodes a list of languages from a JSONArray
	 *
	 * @param	languageArray	A JSONArray object containing the languages.
	 * @return	A list of the languages in the JSONArray
	 */
	public static List<Language> decodeJSONArray(JSONArray languageArray) {
		List<Language> languages = new ArrayList<Language>();
		if (languageArray != null) {
			for (Object langObj : languageArray) {
				if (langObj instanceof JSONObject) {
					JSONObject jsonLangObj = (JSONObject) langObj;
					Language lang = new Language(
							jsonLangObj.get("name").toString(),
							jsonLangObj.get("code").toString());
					languages.add(lang);
				} else {
					String langCode = (String) langObj;
					Language lang = new Language(
							Aikuma.getLanguageCodeMap().get(langCode),
							langCode);
					languages.add(lang);
				}
				
			}
		}
		return languages;
	}

	/**
	 * Compares the given object with the Language, and returns true if the
	 * Language name and code are equal.
	 *
	 * @param	obj	The object to be compared.
	 * @return	true if the name and codes of the languages are equal; false
	 * otherwise.
	 */
	public boolean equals(Object obj) {
		if (obj == null) { return false; }
		if (obj == this) {return true; }
		if (obj.getClass() != getClass()) {
			return false;
		}
		Language rhs = (Language) obj;
		return new EqualsBuilder()
				.append(name, rhs.name).append(code, rhs.code).isEquals();
	}


	/**
	 * Compares the given language with this language
	 *
	 * @param	that	Language object compared with this object
	 * @return			compare result of language
	 */
	public int compareTo(Language that) {
		return name.compareTo(that.getName());
	}
	
} public class Recording extends FileModel {

	// String tag for debugging
	private static final String TAG = "Recording";
	
	/** sample file duration in sec */
	public static final long SAMPLE_SEC = 15;
	
	/**
	 * The constructor used when first creating a Recording.
	 *
	 * @param	recordingUUID	the temporary UUID of the recording in question.
	 * 							(recording can be wav or movie(mp4))
	 * @param	imageUUID		the temporary UUID of the recording-image
	 * @param	name	The recording's name.
	 * @param	comments	The optional free text string
	 * @param	date	The date of creation.
	 * @param	versionName	The recording's version(v0x)
	 * @param	ownerId	The recording owner's ID(Google account)
	 * @param	languages	The languages associated with the recording (used to determine fileType)
	 * @param	deviceName	The model name of the device
	 * @param	androidID	The android ID of the device that created the
	 * recording
	 * @param	groupId	The ID of the group of recordings this recording
	 * belongs in (Some source recording and respeakings/commentaries)
	 * @param	sourceVerId	The version-ID of the source recording of this recording
	 * @param	sampleRate	The sample rate of the recording.
	 * @param	durationMsec	The duration of the recording in milliseconds.
	 * @param	format	The mime type
	 * @param	bitsPerSample	The bits per sample of the audio
	 * @param	numChannels	The number of channels of the audio
	 * @param	latitude The location data
	 * @param	longitude The location data
	 */
	public Recording(UUID recordingUUID, UUID imageUUID, String name, String comments, Date date,
			String versionName, String ownerId,
			List<Language> languages,
			String deviceName, String androidID, String groupId, String sourceVerId,
			long sampleRate, int durationMsec, String format, int numChannels, 
			int bitsPerSample, Double latitude, Double longitude) {
		super(versionName, ownerId, null, null, format);	// id, fileType is defined by Recording class
		this.recordingUUID = recordingUUID;
		this.imageUUID = imageUUID;
		setName(name);
		setComments(comments);
		setDate(date);
		setDeviceName(deviceName);
		setAndroidID(androidID);
		setSampleRate(sampleRate);
		setDurationMsec(durationMsec);
		setGroupId(groupId);
		this.sourceVerId = sourceVerId;
		this.numChannels = numChannels;
		this.bitsPerSample = bitsPerSample;
		this.latitude = latitude;
		this.longitude = longitude;
		// If there isn't an group Id, ie this is an original
		if (groupId == null) {
			setGroupId(createGroupId());
			setRespeakingId("");
		} else {
			// Then we must generate the 6 digit respeaking ID.
			setRespeakingId(IdUtils.randomDigitString(6));
		}
		setFileType(sourceVerId, languages);
		setId(determineId(fileType));
	}

	/**
	 * The constructor used when reading in an existing Recording.
	 *
	 * @param	name	The recording's name.
	 * @param	date	The date of creation.
	 * @param	versionName	The recording's version(v0x)
	 * @param	ownerId	The recording owner's ID(Google account)
	 * @param	sourceVerId	The source's version and Id (if not respeaking, null)
	 * @param	format		The file format
	 * @param	fileType	The file type
	 * @param	androidID	The android ID of the device that created the
	 * recording
	 * @param	groupId	The ID of the group of recordings this recording
	 * belongs in (Some source recording and respeakings/commentaries)
	 * @param	respeakingId	The ID of the recording that this recording
	 * is a respeaking of
	 * @param	sampleRate	The sample rate of the recording.
	 * @param	durationMsec	The duration of the recording in milliseconds.
	 */
	public Recording(String name, Date date,
			String versionName, String ownerId, String sourceVerId,
			String androidID, String groupId, String respeakingId,
			long sampleRate, int durationMsec, String format, String fileType) {
		super(versionName, ownerId, null, null, format);	// id, fileType is defined by Recording class
		setName(name);
		setDate(date);
		setAndroidID(androidID);
		setSampleRate(sampleRate);
		setDurationMsec(durationMsec);
		setGroupId(groupId);
		setRespeakingId(respeakingId);
		this.sourceVerId = sourceVerId;
		this.fileType = fileType;
		setId(determineId(fileType));
	}
	
	/**
	 * The constructor used when creating a segment file
	 *
	 * @param segmentUUID	the temporary UUID of the segment.
	 * @param name			The segment's name.
	 * @param date			The date of creation.
	 * @param versionName	The segment's version(v0x)
	 * @param ownerId		The segment owner's ID(Google account)
	 * @param sourceVerId	The source's version and Id 
	 * @param androidID		The android ID of the device that created the segment
	 * @param groupId		The ID of the group of recordings this segment belongs to
	 * @param format		The file format (JSON)
	 */
	public Recording(UUID segmentUUID, String name, Date date,
			String versionName, String ownerId, String sourceVerId, 
			String androidID, String groupId, 
			String format) {
		super(versionName, ownerId, null, null, format);
		this.recordingUUID = segmentUUID;
		setName(name);
		setDate(date);
		setAndroidID(androidID);
		setGroupId(groupId);
		// Then we must generate the 6 digit respeaking ID.
		setRespeakingId(IdUtils.randomDigitString(6));
		this.sourceVerId = sourceVerId;
		setFileType(sourceVerId, null);
		setId(determineId(fileType));
	}

	

	private String determineId(String fileType) {
		// Build up the filename prefix
		StringBuilder id = new StringBuilder();
		id.append(getGroupId());
		id.append("-");
		id.append(getOwnerId());
		id.append("-");
		if (isOriginal()) {
			id.append(SOURCE_TYPE);
		} else {
			id.append(fileType);
			id.append("-");
			id.append(respeakingId);
		}
		return id.toString();
	}

	/**
	 * Returns true if the Recording is an original; false if respeaking
	 *
	 * @return	True if the recording is an original.
	 */
	public boolean isOriginal() {
		return respeakingId == null || respeakingId.length() == 0;
	}

	// Moves a WAV file with a temporary UUID from a no-sync directory to
	// its rightful place in the connected world of Aikuma, with a proper name
	// and where it will find it's best friend - a JSON metadata file.
	private void importWav(UUID wavUUID, String id) throws IOException {
		importWav(wavUUID + "." + AUDIO_EXT, id + "." + AUDIO_EXT);
	}
	
	private void importWav(String wavUUIDExt, String idExt)
			throws IOException {
		File wavFile = new File(getNoSyncRecordingsPath(), wavUUIDExt);
		Log.i(TAG, "importwav: " + wavFile.length());
		FileUtils.moveFile(wavFile, this.getFile(idExt));
		Log.i(TAG, wavFile.getAbsolutePath() + " move to " + this.getFile(idExt).getAbsolutePath());
	}
	
	// Moves a video File from a no-sync directory to its rightful place
	private void importMov(UUID videoUUID, String id) 
			throws IOException {
		File movFile = new File(getNoSyncRecordingsPath(), videoUUID + "." + VIDEO_EXT);
		FileUtils.moveFile(movFile, this.getFile());
		Log.i(TAG, movFile.getAbsolutePath() + " move to " + this.getFile().getAbsolutePath());
	}
	
	// Moves a recording-image file from a no-sync directory to its rightful place
	private void importImage(UUID imageUUID) throws IOException {
		// First import the full sized image
		File imageFile = ImageUtils.getNoSyncImageFile(imageUUID);
		FileUtils.moveFile(imageFile, getImageFile());
	}

	// Similar to importWav, except for the mapping file.
	private void importMapping(UUID wavUUID, String id)
			throws IOException {
		File mapFile = new File(getNoSyncRecordingsPath(), wavUUID + ".map");
		FileUtils.moveFile(mapFile, getMapFile());
	}
	
	// Similar to importWav
	private void importSegment(UUID segUUID, String id)
			throws IOException {
		File mapFile = new File(getNoSyncRecordingsPath(), segUUID + ".map");
		FileUtils.moveFile(mapFile, this.getFile(id + "." + JSON_EXT));
	}	

	// Create a group ID (the prefix for recordings)
	private String createGroupId() {
		return IdUtils.sampleFromAlphabet(ITEM_ID_LEN, "abcdefghijklmnopqrstuvwxyz");
	}

	/**
	 * Returns a File that refers to the actual recording file.
	 *
	 * @return	The file the recording is stored in.
	 */
	public File getFile() {
		return getFile(id + "." + extension);
	}
	
	private File getFile(String idExt) {
		return new File(getRecordingsPath(), getGroupId() + "/"
				+ idExt);
	}
	
	/**
	 * Returns a File that refers to the recording's transcript file.
	 *
	 * @return	The transcript file of the recording.
	 */
	public File getTranscriptFile() {
		File f = new File(getRecordingsPath(), getGroupId() + "/"
				+ getTranscriptId() + "." + JSON_EXT);
		if(f.exists())
			return f;
		else
			return null;
	}
	
	/**
	 * Returns a File that refers to the recording's preview file.
	 *
	 * @return	The preview(sample) file of the recording.
	 */
	public File getPreviewFile() {
		File f = new File(getRecordingsPath(), getGroupId() + "/"
				+ getPreviewId() + "." + AUDIO_EXT);
		if(f.exists())
			return f;
		else
			return null;
	}
	
	/**
	 * Returns a File that refers to the recording's image file
	 * 
	 * @return The recording-image file, which might not exist
	 */
	public File getImageFile() {
		return new File(getRecordingsPath(), getGroupId() + "/"
				+ getImageId() + "." + IMAGE_EXT);
	}

	/**
	 * Gets the recording's image.
	 *
	 * @return	A Bitmap object.
	 * @throws	IOException	If the image cannot be retrieved.
	 */
	public Bitmap getImage() throws IOException {
		return ImageUtils.retrieveFromFile(getImageFile());
	}
	
	/**
	 * Returns a File that refers to the respeaking's mapping file.
	 *
	 * @return	The mapping file of the respeaking.
	 */
	public File getMapFile() {
		if(isOriginal())
			return null;
		return new File(getRecordingsPath(), getGroupId() + "/" +
				id + getSuffixExt(versionName, FileType.MAPPING));
	}
	
	
	public String getMapId() {
		return (id + MAPPING_SUFFIX);
	}
	
	public String getPreviewId() {
		return (id + SAMPLE_SUFFIX);
	}
	
	public String getImageId() {
		return (id + IMAGE_SUFFIX);
	}
	
	public String getTranscriptId() {
		return (getGroupId() + "-" + getOwnerId() + TRANSCRIPT_SUFFIX);
	}
	
	/**
	 * Returns an identifier used in cloud-storage
	 * @return	a relative-path of recording to 'aikuma/'
	 */
	public String getCloudIdentifier() {
		String ownerDirStr = (versionName + "/" + ownerId + "/");
		return (ownerDirStr + PATH + getGroupId() + "/" + id + "." + extension);
	}


	/**
	 * Name accessor; returns an empty string if the name is null
	 *
	 * @return	The name of the recording.
	 */
	public String getName() {
		if (name != null) {
			return name;
		} else {
			return "";
		}
	}
	
	/**
	 * Comments accessor; returns an empty string if the comments is null
	 * 
	 * @return	The comments of the recording
	 */
	public String getComments() {
		if (comments == null)
			return "";
		return comments;
	}

	public Date getDate() {
		return date;
	}

	/**
	 * Languages accessor; return a list of languages associated with the recording
	 * It looks for all language tag files under aikuma folder 
	 * 
	 * @return	a list of languages
	 */
	public List<Language> getLanguages() {
		if(languagesBuffer.size() != 0)
			return languagesBuffer;
		
		final Map<String, String> languageCodeMap = Aikuma.getLanguageCodeMap();
		
		filterTags(LANGUAGE_TAG_TYPE, new TagProcessor() {
			@Override
			public void processTag(String verGroupOwnerStr, String tagStr) {
				if(tagStr.length() > 7 && tagStr.startsWith("iso639_")) {
					String langCode = tagStr.substring(7);
					languagesBuffer.add(
							new Language(languageCodeMap.get(langCode), langCode));
				} else {
					languagesBuffer.add(new Language(tagStr, ""));
				}
			}
			
		});
		
		return languagesBuffer;
	}

	/**
	 * Returns the first language code as a string, or an empty string if there
	 * is none.
	 *
	 * @return	The language code of the first language associated with the
	 * recording.
	 */
	public String getFirstLangCode() {
		if (getLanguages().size() > 0) {
			return getLanguages().get(0).getCode();
		} else {
			return "";
		}
	}

	/**
	 * Returns the name and language of the recording in a single string.
	 *
	 * @return	The name and langugage of the recording in a string.
	 */
	public String getNameAndLang() {
		if (getFirstLangCode().equals("")) {
			return getName();
		} else {
			return getName() + " (" + getFirstLangCode() + ")";
		}
	}

	/**
	 * speakersIds accessor.
	 *
	 * @return	A list of IDs representing the speakers of the recording.
	 */
	public List<String> getSpeakersIds() {
		if(speakersIdsBuffer.size() != 0)
			return speakersIdsBuffer;
		
		filterTags(SPEAKER_TAG_TYPE, new TagProcessor() {
			@Override
			public void processTag(String verGroupOwnerStr, String tagStr) {
				String tagCreator = verGroupOwnerStr.substring(Recording.ITEM_ID_LEN + 5);
				speakersCreatorsBuffer.add(tagCreator);
				speakersIdsBuffer.add(tagStr);
			}	
		});
		
		return speakersIdsBuffer;
	}
	
	/**
	 * speakers' file-models accessor.
	 * @return	a list of File-model instances of the recording's speakers
	 *          (Speakers will always have the same version with the recording)
	 */
	public List<Speaker> getSpeakers() {
		if(speakersIdsBuffer.size() == 0) {
			getSpeakersIds();
		}
		Log.i(TAG, speakersIdsBuffer.toString());
		//List<FileModel> speakers = new ArrayList<FileModel>();
		List<Speaker> speakers = new ArrayList<Speaker>();
		for(int i = 0; i < speakersIdsBuffer.size(); i++) {
			String creatorId = speakersCreatorsBuffer.get(i);
			String speakerId = speakersIdsBuffer.get(i);
			//speakers.add(new FileModel(this.versionName, creatorId, speakerId, SPEAKER_TYPE, IMAGE_EXT));
			try {
				speakers.add(Speaker.read(this.versionName, creatorId, speakerId));
			} catch(IOException e) {
				Log.e(TAG, speakerId + " file can't be read under the folder of " + creatorId);
			}
		}
		
		return speakers;
	}
	
	/**
	 * Get a list of OLAC tag strings associated with the recording
	 * 
	 * @return	a list of OLAC tag strings
	 */
	public List<String> getOLACTagStrings() {
		if(olacTagStringsBuffer.size() != 0)
			return olacTagStringsBuffer;
		
		olacTagStringsBuffer = new ArrayList<String>();
		
		filterTags(OLAC_TAG_TYPE, new TagProcessor() {
			@Override
			public void processTag(String verGroupOwnerStr, String tagStr) {
				olacTagStringsBuffer.add(tagStr);
			}
		});
		
		return olacTagStringsBuffer;
	}
	
	/**
	 * Get a list of custom tag strings associated with the recording
	 * 
	 * @return	a list of custom tag strings
	 */
	public List<String> getCustomTagStrings() {
		if(customTagStringsBuffer.size() != 0)
			return customTagStringsBuffer;
		
		customTagStringsBuffer = new ArrayList<String>();
		
		filterTags(CUSTOM_TAG_TYPE, new TagProcessor() {
			@Override
			public void processTag(String verGroupOwnerStr, String tagStr) {
				customTagStringsBuffer.add(tagStr);
			}
		});
		
		return customTagStringsBuffer;
	}
	
	/** 
	 * Get a list of FileModels of all tags associated with the recording
	 * 
	 * @return	a list of FileModel structures of all tags
	 */
	public List<FileModel> getTags() {
		final List<FileModel> tagFileModels = new ArrayList<FileModel>();
		
		filterTags(ALL_TAG_TYPE, new TagProcessor() {
			@Override
			public void processTag(String verGroupOwnerStr, String tagFileName) {
				String[] splitVerGroupOwnerStr = verGroupOwnerStr.split("-");
				if(splitVerGroupOwnerStr.length > 3) {
					for(int i = 3; i < splitVerGroupOwnerStr.length; i++) {
						splitVerGroupOwnerStr[2] += splitVerGroupOwnerStr[i];
					}
				}
				tagFileModels.add(new FileModel(splitVerGroupOwnerStr[0], 
						splitVerGroupOwnerStr[2], tagFileName, FileModel.TAG_TYPE, ""));
			}
			
		});
		
		return tagFileModels;
	}
	
	/**
	 * Returns true if the recording is a movie file
	 * (Currently movie file is only stored with .mp4 extension
	 * 
	 * @return	true if this is a movie
	 */
	public boolean isMovie() {
		return this.extension.equals(VIDEO_EXT);
	}

	/**
	 * androidID accessor
	 *
	 * @return	The Andorid of the device that made the recording.
	 */
	public String getAndroidID() {
		return androidID;
	}

	/**
	 * groupId accessor.
	 *
	 * @return	The Id of the group this recording belongs in.
	 * of.
	 */
	public String getGroupId() {
		return groupId;
	}

	public String getRespeakingId() {
		return respeakingId;
	}

	/**
	 * sampleRate accessor
	 *
	 * @return	The sample rate of the recording as a long.
	 */
	public long getSampleRate() {
		return sampleRate;
	}

	/**
	 * durationMsec accessor
	 *
	 * @return	The duration of the recording in milliseconds as an int.
	 */
	public int getDurationMsec() {
		return durationMsec;
	}

	/**
	 * Encode the Recording as a corresponding JSONObject.
	 *
	 * @return	A JSONObject instance representing the Recording;
	 */
	public JSONObject encode() {
		JSONObject encodedRecording = new JSONObject();
		
		encodedRecording.put(NAME_KEY, this.name);
		encodedRecording.put(DATE_KEY, new StandardDateFormat().format(this.date));
		encodedRecording.put(VERSION_KEY, this.versionName);
		encodedRecording.put(USER_ID_KEY, this.ownerId);
		encodedRecording.put(ANDROID_ID_KEY, this.androidID);
		encodedRecording.put(ITEM_ID_KEY, this.groupId);
		encodedRecording.put(RESPEAKING_ID_KEY, this.respeakingId);
		encodedRecording.put(SOURCE_VER_ID_KEY, this.sourceVerId);
		
		encodedRecording.put(FILE_TYPE_KEY, getFileType());
		encodedRecording.put(FORMAT_KEY, this.format);
		
		if(!fileType.equals(SEGMENT_TYPE)) {
			encodedRecording.put(COMMENTS_KEY, this.comments);
			encodedRecording.put(DEVICE_NAME_KEY, deviceName);
			encodedRecording.put(SAMPLERATE_KEY, getSampleRate());
			encodedRecording.put(DURATION_MSEC_KEY, getDurationMsec());
			encodedRecording.put(BITS_PER_SAMPLE_KEY, this.bitsPerSample);
			encodedRecording.put(NUM_CHANNELS_KEY, this.numChannels);
			
			if(latitude != null && longitude != null) {
				JSONArray locationData = new JSONArray();
				locationData.add(latitude);
				locationData.add(longitude);
				encodedRecording.put(LOCATION_KEY, locationData);
			} else {
				encodedRecording.put(LOCATION_KEY, null);
			}
		}
		
		return encodedRecording;
	}

	/**
	 * Make an index-file at the application root path
	 * 
	 * @param srcVerId		(source's versionName)-(source's ID)
	 * @param respkVerId	(respeaking's versionName)-(respeaking's ID)
	 * @throws IOException	thrown by read/write function
	 */
	private static void index(String srcVerId, String respkVerId) throws IOException {
		File indexFile = new File(FileIO.getAppRootPath(), "index.json");
		JSONObject indices = null;
		try {
			indices = FileIO.readJSONObject(indexFile);
		} catch (IOException e) {
			indices = new JSONObject();
		}
		
		String srcId = srcVerId.substring(4);
		String[] splitRespeakName = respkVerId.split("-");
		String val = splitRespeakName[0] + "-" + splitRespeakName[1] + "-" + splitRespeakName[2];
		
		JSONArray values = (JSONArray) indices.get(srcId);
		if(values == null) {
			values = new JSONArray();
		}
		if(!values.contains(val)) {
			values.add(val);
			indices.put(srcId, values);
			FileIO.writeJSONObject(indexFile, indices);
		}
	}
	
	/**
	 * Write the recording's metadata string to path
	 * 
	 * @param path				Path to a file of the metadata
	 * @param metadataJSONStr	JSON string of metadata
	 * @throws IOException		thrown by write/index functions
	 */
	public static void write(File path, String metadataJSONStr) throws IOException {
		FileIO.write(path, metadataJSONStr);
		
		JSONParser parser = new JSONParser();
		JSONObject jsonObj;
		try {
			jsonObj = (JSONObject) parser.parse(metadataJSONStr);
		} catch (org.json.simple.parser.ParseException e) {
			throw new IOException(e);
		}
		
		
		String jsonSrcVerId = (String) jsonObj.get(SOURCE_VER_ID_KEY);
		// if the recording is a respeaking.
		if(jsonSrcVerId != null) {
			String jsonVerName = (String) jsonObj.get(VERSION_KEY);
			String jsonGroupId = (String) jsonObj.get(ITEM_ID_KEY);
			String jsonOwnerId = (String) jsonObj.get(USER_ID_KEY);
			
			// Write the index file
			index(jsonSrcVerId, jsonVerName + "-" + jsonGroupId + "-" + jsonOwnerId);
		}
	}
	
	/**
	 * Write the Recording to file in a subdirectory of the recordings and move
	 * the recording WAV data to that directory
	 *
	 * @throws	IOException	If the recording metadata could not be written.
	 */
	public void write() throws IOException {
		// Ensure the directory exists
		File dir = new File(getRecordingsPath(), getGroupId());
		dir.mkdir();
		Log.i(TAG, "write: " + dir.getAbsolutePath());
		// Import the wave file into the new recording directory.
		if(this.isMovie()) {
			importMov(recordingUUID, getId());
		} else if (extension.equals(AUDIO_EXT)){
			Log.i("COPY", recordingUUID.toString());
			importWav(recordingUUID, getId());	
		}
		

		// if the recording is original
		if (isOriginal()) {
			// Import the sample wave file into the new recording directory
			String suffixExt = getSuffixExt(versionName, FileType.PREVIEW);
			importWav(recordingUUID + suffixExt, getId() + suffixExt);
			if(imageUUID != null)	// image is optional
				importImage(imageUUID);
		} else {
			// Try and import the mapping file
			if(fileType.equals(SEGMENT_TYPE))
				importSegment(recordingUUID, getId());
			else
				importMapping(recordingUUID, getId());
			
			// Write the index file
			index(sourceVerId, getVersionName() + "-" + getId());
		}

		JSONObject encodedRecording = this.encode();
		encodedRecording.put(CHECKSUM_KEY, IdUtils.getMD5Hash(getFile()));


		// Write the json metadata.
		FileIO.writeJSONObject(new File(
				getRecordingsPath(), getGroupId() + "/" +
						id + getSuffixExt(versionName, FileType.METADATA)),
				encodedRecording);
	}

	
	/**
	 * Deletes the JSON File associated with the recording.
	 *
	 * @return	true if successful; false otherwise.
	 */
	 /*
	public boolean delete() {
		File file = new File(getRecordingsPath(), this.getUUID().toString() +
				".json");
		if (!isOriginal()) {
			File mapFile = new File(getRecordingsPath(),
					this.getUUID().toString() + ".map");
			boolean result;
			result = mapFile.delete();
			if (!result) {
				return false;
			}
		}
		return file.delete();
	}
	*/

	/**
	 * Returns this recordings original.
	 *
	 * @return	The original recording
	 * @throws	IOException	If there is an issue reading the originals JSON
	 * file
	 */
	public Recording getOriginal() throws IOException {
		String[] splitSourceName = sourceVerId.split("-");
		File ownerDir = 
				FileIO.getOwnerPath(splitSourceName[0], splitSourceName[2]);
		File groupDir = 
				new File(getRecordingsPath(ownerDir), getGroupId());

		// Filter for files that are source metadata
		File[] groupMetadataFileArray = groupDir.listFiles(
				new FilenameFilter() {
			public boolean accept(File dir, String filename) {
				String[] splitFilename = filename.split("-");
				if (splitFilename[2].equals("source") &&
					filename.endsWith(getSuffixExt(versionName, FileType.METADATA))) {
					return true;
				}
				return false;
			}
		});

		assertTrue(groupMetadataFileArray.length == 1);

		return Recording.read(groupMetadataFileArray[0]);
	}

	/**
	 * Read a recording of the given ID 
	 * (assuming that the file and aikuma are in the same version)
	 * 
	 * @param 	id			The recording's ID
	 * @return	A recording object corresponding to the ID
	 * @throws IOException	if the recording metadata file cannot be read
	 */
	public static Recording read(String id) throws IOException {
		String[] splitId = id.split("-");
		return Recording.read(AikumaSettings.getLatestVersion(), splitId[1], id);
	}
	
	/**
	 * Read a recording corresponding to the given filename prefix.
	 *
	 * @param	verName			The recording's versionName
	 * @param	ownerAccount	The recording's ownerID
	 * @param	id				The recording's ID
	 * @return	A Recording object corresponding to the json file.
	 * @throws	IOException	If the recording metadata cannot be read.
	 */
	public static Recording read(String verName, String ownerAccount, 
			String id) throws IOException {
		String groupId = getGroupIdFromId(id);
		File ownerDir = FileIO.getOwnerPath(verName, ownerAccount);
		File metadataFile = 
				new File(getRecordingsPath(ownerDir),
						groupId + "/" + id + getSuffixExt(verName, FileType.METADATA));
		Log.i(TAG, metadataFile.getAbsolutePath());
		return read(metadataFile);
	}

	/**
	 * Read a recording from the file containing JSON describing the Recording
	 *
	 * @param	metadataFile	The file containing the metadata of the recording.
	 * @return	A Recording object corresponding to the json file.
	 * @throws	IOException	If the recording metadata cannot be read.
	 */
	public static Recording read(File metadataFile) throws IOException {
		JSONObject jsonObj = FileIO.readJSONObject(metadataFile);
		
		return read(jsonObj);
	}
	
	/**
	 * Read a recording from JSON object describing the Recording
	 *
	 * @param	jsonObj			JSON Object containing the metadata of the recording.
	 * @return	A Recording object corresponding to the json object.
	 * @throws	IOException	If the recording metadata doesn't exist.
	 */
	public static Recording read(JSONObject jsonObj) throws IOException {
		String groupId = (String) jsonObj.get(ITEM_ID_KEY);

		if (groupId == null) {
			throw new IOException("Null groupId in the JSON file.");
		}
		String name = (String) jsonObj.get(NAME_KEY);
		String dateString = (String) jsonObj.get(DATE_KEY);
		if (dateString == null) {
			throw new IOException("Null date in the JSON file.");
		}
		Date date;
		try {
			if(dateString.matches("^\\d{8}$")) {
				date = new SimpleDateFormat("yyyyMMdd").parse(dateString);
			} else {
				date = new StandardDateFormat().parse(dateString);
			}
			
		} catch (ParseException e) {
			throw new IOException(e);
		}
		String versionName = (String) jsonObj.get(VERSION_KEY);
		String ownerId = (String) jsonObj.get(USER_ID_KEY);
		String sourceVerId = (String) jsonObj.get(SOURCE_VER_ID_KEY);
		String format = (String) jsonObj.get(FORMAT_KEY);
		String fileType = (String) jsonObj.get(FILE_TYPE_KEY);
		String androidID = (String) jsonObj.get(ANDROID_ID_KEY);
		if (androidID == null) {
			throw new IOException("Null androidID in the JSON file.");
		}
		String respeakingId = (String) jsonObj.get(RESPEAKING_ID_KEY);
//		if (respeakingId == null) { // can be null when the metadata is read from cloud-fullText
//			throw new IOException("Null respeakingId in the JSON file.");
//		}

		long sampleRate;
		if (jsonObj.get(SAMPLERATE_KEY) == null) {
			sampleRate = -1;
		} else {
			sampleRate = (Long) jsonObj.get(SAMPLERATE_KEY);
		}

		int durationMsec;
		if (jsonObj.get(DURATION_MSEC_KEY) == null) {
			if(jsonObj.get(DURATION_CLOUD_KEY) == null) {
				durationMsec = -1;
				Log.i(TAG, "reading: null");
			} else {
				String durationRangeStr = (String) jsonObj.get(DURATION_CLOUD_KEY);
				durationMsec = DurRange.valueOf(durationRangeStr).getValue();
			}
			
		} else {
			durationMsec = ((Long) jsonObj.get(DURATION_MSEC_KEY)).intValue();
			Log.i(TAG, "reading: " + durationMsec);
		}
		Recording recording = new Recording(name, date, versionName, ownerId, 
				sourceVerId, /*languages, speakersIds, */androidID, groupId, 
				respeakingId, sampleRate, (Integer) durationMsec, format, fileType);
		return recording;
	}

	/**
	 * Returns a list of all the respeakings of this Recording. Ff it is a
	 * respeaking it will return an list of the other respeakings in the
	 * group.
	 * (If this is called by a respeaking, empty arraylist is returned)
	 *
	 * @return	A list of all the respeakings of the recording. 
	 * 			(an error can return an empty list)
	 *          (respekaings will always have the same version with original)
	 */
	public List<Recording> getRespeakings() {
		List<File> groupDirList = new ArrayList<File>();
		List<Recording> respeakings = new ArrayList<Recording>();
		if(!this.isOriginal()) return respeakings;
		
		File indexFile = new File(FileIO.getAppRootPath(), "index.json");
		JSONObject indices = null;
		try {
			indices = FileIO.readJSONObject(indexFile);
		} catch (IOException e1) {
			// TODO: How to deal with no index-file?
			Log.e(TAG, "getRespeakings(): error in reading index file");
			indices = new JSONObject();
			//return respeakings;
		}
		
		// Collect directories where related respeakings exist form index file
		JSONArray respkList = (JSONArray) indices.get(getId());
		if(respkList == null) {
			// TODO: How to deal with no index-file?
			respkList = new JSONArray();
			//return respeakings;
		}
		
		for (int i = 0; i < respkList.size(); i++) {
			String[] splitRespkName = ((String)respkList.get(i)).split("-");
			if(!splitRespkName[0].matches(versionName))
				continue;
			
			File ownerDir = 
					FileIO.getOwnerPath(splitRespkName[0], splitRespkName[2]);
			File groupDir = 
					new File(getRecordingsPath(ownerDir), splitRespkName[1]);
			//Check if this index is created for derivative recordings, not for tags 
			if(groupDir.exists())	
				groupDirList.add(groupDir);
		}
		// For the respeakings existing in the same folder of original
		File currentDir = new File(getRecordingsPath(), getGroupId());
		if(!groupDirList.contains(currentDir)) {
			groupDirList.add(currentDir);
		}
		
		// Read metadata files
		for(File groupDir: groupDirList) {

			File[] groupDirMetaFiles = groupDir.listFiles(new FilenameFilter() {
				public boolean accept(File dir, String filename) {
					return filename.endsWith(getSuffixExt(versionName, FileType.METADATA));
				}
			});
	
			if(groupDirMetaFiles != null) {
				Recording recording;
				for (File recordingMetaFile : groupDirMetaFiles) {
					try {
						recording = Recording.read(recordingMetaFile);
						if (!recording.isOriginal()) {
							respeakings.add(recording);
						}
					} catch (IOException e) {
						// Well we can't read the recordings metadata file, so just
						// continue on.
					}
				}
			}
			
		}
		
		return respeakings;
	}
	
	/**
	 * read all tags in the device
	 * @return	a list of FileModel structures of all tags
	 */
	public static List<FileModel> readAllTags() {
		List<FileModel> tags = new ArrayList<FileModel>();
		
		// Get a list of version directories
		final String currentVersionName = AikumaSettings.getLatestVersion();
		File[] versionDirs = 
				FileIO.getAppRootPath().listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String filename) {
				//return filename.startsWith("v") && filename.substring(1).matches("\\d+");
				return filename.matches(currentVersionName);
			}	
		});

		for(File f1 : versionDirs) {
			String versionName = f1.getName();
			//File[] firstHashDirs = f1.listFiles();
			//for(File f2 : firstHashDirs) {
				//File[] secondHashDirs = f2.listFiles();
				//for(File f3 : secondHashDirs) {
					File[] ownerIdDirs = f1.listFiles();
					for(File f : ownerIdDirs) {
						if(f.getName().equals(AdminFileModel.ADMIN))
							continue;
						addTagsInDir(versionName, tags, f);
					}
				//}
			//}
		}

		return tags;		
	}
	
	private static void addTagsInDir(final String versionName, 
			List<FileModel> tags, File ownerDir) {
		// Constructs a list of directories in the recordings directory.
		File[] recordingPathFiles = getTagsPath(ownerDir).listFiles();
		
		if (recordingPathFiles == null) {
			return;
		}
			
		for (File f : recordingPathFiles) {
			if (f.isDirectory()) {	// f is a item directory under social/
				File[] tagFiles = f.listFiles();

				for (File tagFile : tagFiles) {
					FileModel fm = new FileModel(versionName, ownerDir.getName(), 
							tagFile.getName(), FileModel.TAG_TYPE, "");
					tags.add(fm);
				}
			}
		}
	}

	/**
	 * Read all recordings
	 *
	 * @return	A list of all the recordings in the Aikuma directory.
	 */
	public static List<Recording> readAll() {
		return readAll(null);
	}
	
	/**
	 * Read all recordings of the user
	 *
	 * @param userId	The user's ID
	 * @return	A list of all the user's recordings in the Aikuma directory.
	 */
	public static List<Recording> readAll(String userId) {

		List<Recording> recordings = new ArrayList<Recording>();
		
		// Get a list of version directories
		final String currentVersionName = AikumaSettings.getLatestVersion();
		File[] versionDirs = 
				FileIO.getAppRootPath().listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String filename) {
				//return filename.startsWith("v") && filename.substring(1).matches("\\d+");
				return filename.matches(currentVersionName);
			}	
		});

		for(File f1 : versionDirs) {
			String versionName = f1.getName();
			//File[] firstHashDirs = f1.listFiles();
			//for(File f2 : firstHashDirs) {
				//File[] secondHashDirs = f2.listFiles();
				//for(File f3 : secondHashDirs) {
					File[] ownerIdDirs = f1.listFiles();
					for(File f : ownerIdDirs) {
						String dirName = f.getName();
						if(dirName.equals(AdminFileModel.ADMIN))
							continue;
						
						if(userId == null || dirName.equals(userId)) {
							Log.i(TAG, "readAll: " + f.getPath());
							addRecordingsInDir(versionName, recordings, f);
						}
						
					}
				//}
			//}
		}

		return recordings;
	}

	private static void addRecordingsInDir(final String versionName, 
			List<Recording> recordings, File dir) {
		// Constructs a list of directories in the recordings directory.
		File[] recordingPathFiles = getRecordingsPath(dir).listFiles();
		
		if (recordingPathFiles == null) {
			return;
		}
			
		for (File f : recordingPathFiles) {
			if (f.isDirectory()) {
				// For each of those subdirectories, creates a list of files
				// within that end in .json
				File[] groupDirFiles = f.listFiles(new FilenameFilter() {
					public boolean accept(File dir, String filename) {
						return filename.endsWith(getSuffixExt(versionName, FileType.METADATA));
					}
				});

				// Iterate over those recording metadata files and add the
				// recordings they refer to to the recordings list
				for (File jsonFile : groupDirFiles) {
					try {
						Recording rec = Recording.read(jsonFile);
						recordings.add(rec);
					} catch (IOException e) {
						// Couldn't read that recording for whateve rreason
						// (perhaps json file wasn't formatted correctly).
						// Let's just ignore that user.
						Log.e(TAG, "read exception(" + e.getMessage() + "): " + jsonFile.getName());
					}
				}
			}
		}
	}
	
	/**
	 * Index all recordings and create a index file
	 * 
	 * @throws IOException	while writing a index file
	 */
	public static void indexAll() throws IOException {
		File indexFile = new File(FileIO.getAppRootPath(), "index.json");
		
		JSONObject indices = new JSONObject();
		List<String> recordingIds = new ArrayList<String>();
		JSONObject itemDirPaths = new JSONObject();

		// Get a list of version directories
		File[] versionDirs = 
				FileIO.getAppRootPath().listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String filename) {
				return filename.startsWith("v") && filename.substring(1).matches("\\d+");
			}	
		});

		for(File f1 : versionDirs) {
			//File[] firstHashDirs = f1.listFiles();
			//for(File f2 : firstHashDirs) {
				//File[] secondHashDirs = f2.listFiles();
				//for(File f3 : secondHashDirs) {
					File[] ownerIdDirs = f1.listFiles();
					for(File f4 : ownerIdDirs) {						
						if(f4.getName().equals(AdminFileModel.ADMIN))
							continue;
						
						Log.i(TAG, "indexAll: " + f4.getPath());
						List<File> itemDirsList = new ArrayList<File>(
								Arrays.asList(getRecordingsPath(f4).listFiles()));
						itemDirsList.addAll(Arrays.asList(getTagsPath(f4).listFiles()));
						
						for(File f5 : itemDirsList) {
							
							if (f5.isDirectory()) {
								// Record the path to this item folder
								String itemName = f5.getName();
								
								JSONArray values = (JSONArray) itemDirPaths.get(itemName);
								String val = f1.getName() + "-" + itemName + "-" + f4.getName();
								if(values == null) {
									values = new JSONArray();
								}
								if(!values.contains(val)) {
									values.add(val);
									itemDirPaths.put(f5.getName(), values);
								}
																					
								// Record all the recordings (original: derivative, tag / derivative: tag)
								File[] files = f5.listFiles(new FilenameFilter() {
									public boolean accept(File dir, String filename) {
										return filename.endsWith(AUDIO_EXT) || filename.endsWith(VIDEO_EXT);
									}
								});
								for(File f : files) {
									String fileName = f.getName();
									String srcId = fileName.substring(0, fileName.lastIndexOf('.'));
									String tempId = srcId.substring(0, srcId.length()-7);
									if(srcId.endsWith(SOURCE_TYPE) || tempId.endsWith(RESPEAKING_TYPE) ||
											tempId.endsWith(TRANSLATION_TYPE)) {
										// At most one source file can exist in one item folder
										if(!recordingIds.contains(srcId))
											recordingIds.add(srcId);
									}	
								}
								
							}
						}
					}
				//}
			//}
		}
		
		for(String srcId : recordingIds) {
			String itemId = srcId.split("-")[0];
			indices.put(srcId, itemDirPaths.get(itemId));
		}
		FileIO.writeJSONObject(indexFile, indices);
	}
	
	/**
	 * Updates all recording metadata files of versionNum
	 * 1. Update or create fields existing in newJSONFields
	 * 2. After 1, Change field keys existing in newJSONKeys(oldkey -> newkey)
	 * 
	 * @param versionNum		obsolete file-format's version
	 * @param newJSONFields		Map structure of new field-pairs(key:value)
	 * @param newJSONKeys		Map structure of new key-pairs(oldkey:newkey)
	 */
	public static void updateAll(Integer versionNum, 
			Map<String, Object> newJSONFields, Map<String, String> newJSONKeys) {
		if(newJSONFields == null && newJSONKeys == null)
			return;
		
		switch(versionNum) {
		case 0:
			File[] firstHashDirs = new File(FileIO.getAppRootPath(), "v01").listFiles();
			
			for(File f2 : firstHashDirs) {
				File[] secondHashDirs = f2.listFiles();
				for(File f3 : secondHashDirs) {
					File[] ownerIdDirs = f3.listFiles();
					for(File f : ownerIdDirs) {
						Log.i(TAG, "updateAll: " + f.getPath());
						
						updateMetadataInDir(f, "v01", newJSONFields, newJSONKeys);	
					}
				}
			}
			return;
		}
	}
	
	private static void updateMetadataInDir(File dir, final String versionName,
			Map<String, Object> newJSONFields, Map<String, String> newJSONKeys) {
		boolean isFields = (newJSONFields != null);
		boolean isKeys = (newJSONKeys != null);
		// Constructs a list of directories in the recordings directory.
		File[] recordingPathFiles = getRecordingsPath(dir).listFiles();
		
		if (recordingPathFiles == null) {
			return;
		}
		
		for (File f : recordingPathFiles) {
			if (f.isDirectory()) {
				// For each of those subdirectories, creates a list of files
				// within that end in .json
				File[] groupDirFiles = f.listFiles(new FilenameFilter() {
					public boolean accept(File dir, String filename) {
						return filename.endsWith(getSuffixExt(versionName, FileType.METADATA));
					}
				});

				// Iterate over those recording metadata files and add the
				// recordings they refer to to the recordings list
				for (File jsonFile : groupDirFiles) {
					try {
						JSONObject newJSONObject = 
								FileIO.readJSONObject(jsonFile);
						
						if(isFields) {
							newJSONObject.putAll(newJSONFields);
						}
						if(isKeys) {
							for(String oldKey : newJSONKeys.keySet()) {
								Object val = newJSONObject.remove(oldKey);
								if(val != null) {
									String newKey = newJSONKeys.get(oldKey);
									newJSONObject.put(newKey, val);
								}
							}
						}
							
						FileIO.writeJSONObject(jsonFile, newJSONObject);
					} catch (IOException e) {
						Log.e(TAG, "Metadata update failed: " + e.toString());
					}
				}
			}
		}
	}
	
	/**
	 * Compares the given object with the Recording, and returns true if the
	 * Recording's name, date, languages, androidID, groupId and
	 * respeakingId are equal
	 *
	 * @param	obj	The object to be compared.
	 * @return	true most fields are the same; false otherwise
	 */
	 public boolean equals(Object obj) {
	 	if (obj == null) {return false;}
		if (obj == this) {return true;}
		if (obj.getClass() != getClass()) {return false;}
		Recording rhs = (Recording) obj;
		return new EqualsBuilder()
				.append(name, rhs.name)
				.append(comments, rhs.comments)
				.append(date, rhs.date)
				.append(androidID, rhs.androidID)
				.append(groupId, rhs.groupId)
				.append(respeakingId, rhs.respeakingId)
				.append(sampleRate, rhs.sampleRate)
				.isEquals();
	 }

	/**
	 * Name mutator.
	 */
	private void setName(String name) {
		this.name = name;
	}
	
	private void setComments(String comments) {
		this.comments = comments;
	}

	// Sets the date; the date cannot be null.
	private void setDate(Date date) {
		if (date == null) {
			throw new IllegalArgumentException(
					"Recording date cannot be null.");
		}
		this.date = date;
	}
	
	// Sets the file-type
	private void setFileType(String sourceVerId, List<Language> languages) {
		if (sourceVerId == null) {
			this.fileType = SOURCE_TYPE;
		} else {
			// Then this is either a respeaking or a translation.
			try {
				if (languages == null) {
					this.fileType = SEGMENT_TYPE;
				}
				else if (languages.equals(getOriginal().getLanguages())) {
					this.fileType = RESPEAKING_TYPE;
				} else {
					this.fileType = TRANSLATION_TYPE;
				}
			} catch (IOException e) {
				// There is an issue reading the original. A type won't be
				// written.
			}
		}
	}

	private void setDeviceName(String deviceName) {
		if (deviceName == null) {
			throw new IllegalArgumentException(
					"The model name cannot be null");
		}
		this.deviceName = deviceName.toUpperCase();
	}
	
	// Sets the android ID but won't accept a null string.
	private void setAndroidID(String androidID) {
		if (androidID == null) {
			throw new IllegalArgumentException(
					"The androidID for the recording cannot be null");
		}
		this.androidID = androidID;
	}

	private void setGroupId(String groupId) {
		this.groupId = groupId;
	}

	/*
	private void setId(String id) {
		this.id = id;
	}*/

	private void setSampleRate(long sampleRate) {
		this.sampleRate = sampleRate;
	}

	private void setRespeakingId(String respeakingId) {
		this.respeakingId = respeakingId;
	}

	private void setDurationMsec(int durationMsec) {
		this.durationMsec = durationMsec;
	}

	/**
	 * Get the applications recordings directory
	 *
	 * @param	ownerDir	A File representing the path of owner's directory
	 * @return	A File representing the path of the recordings directory
	 */
	public static File getRecordingsPath(File ownerDir) {
		File path = new File(ownerDir, PATH);
		path.mkdirs();
		return path;
	}

	/**
	 * Get the recording owner's directory
	 * 
	 * @return	A file representing the path of the recording owner's dir
	 */
	public File getRecordingsPath() {
		File path = new File(
				FileIO.getOwnerPath(versionName, ownerId), PATH);
		path.mkdirs();
		return path;
	}
	
	/**
	 * Get the applications recording directory that isn't synced.
	 *
	 * @return	A File representing the path of the recordings directory in the
	 * no-sync Aikuma directory.
	 */
	public static File getNoSyncRecordingsPath() {
		File path = new File(FileIO.getNoSyncPath(), PATH);
		path.mkdirs();
		return path;
	}
	
	/**
	 * Get the directory of the tags created by the owner
	 * 
	 * @param ownerDir		The owner directory 
	 * @return				The owner's tag directory
	 */
	public static File getTagsPath(File ownerDir) {
		File path = new File(ownerDir, TAG_PATH);
		path.mkdirs();
		return path;
	}
	
	/**
	 * Get this recording owner's tag directory
	 * @return	the tag directory
	 */
	public File getTagsPath() {
		File path = new File(
				FileIO.getOwnerPath(versionName, ownerId), TAG_PATH);
		path.mkdirs();
		return path;
	}

	/**
	 * Returns the groupId given an id
	 *
	 * @param	id	The filename prefix of the file whose
	 * groupId we seek
	 * @return	The corresponding group ID.
	 */
	public static String getGroupIdFromId(String id) {
		String[] splitId = id.split("-");
		assertTrue(splitId.length >= 3);
		return splitId[0];
	}
	
	/**
	 * Star the recording with this phone's androidID
	 * 
	 * @param currentVerName	Current aikuma's version
	 * @param userId			ID of an user who liked this recording
	 * @throws	IOException	In case there is an issue writing the star file.
	 * Note that this will not be thrown if the file already exists.
	 */
	public void star(String currentVerName, String userId) throws IOException {
		String androidID = Aikuma.getAndroidID();
		File starFile = new File(FileIO.getOwnerPath(currentVerName, userId), 
				"/social/" + getId() + "-like");
		starFile.getParentFile().mkdirs();
		starFile.createNewFile();
	}

	/**
	 * Flag the recording with this phone's androidID
	 *
	 * @param currentVerName	Current aikuma's version
	 * @param userId			ID of an user who dis-liked this recording
	 * @throws	IOException	In case there is an issue writing the flag file.
	 * Note that this will not be thrown if the file already exists.
	 */
	public void flag(String currentVerName, String userId) throws IOException {
		String androidID = Aikuma.getAndroidID();
		File flagFile = new File(FileIO.getOwnerPath(currentVerName, userId), 
				"/social/"  + getId() + "-report");
		flagFile.getParentFile().mkdirs();
		flagFile.createNewFile();
	}
	
	/**
	 * Make the archived recording's metadata
	 * @param backupDate	when upload finished
	 * @param downloadUrl	url where the recording file can be downloaded
	 * @throws	IOException	In case of an issue writing the archiveMetadata file.
	 * Note that this will not be thrown if the file already exists.
	 */
	public void archive(String backupDate, String downloadUrl) throws IOException {
		JSONObject archiveMetadata = new JSONObject();
		archiveMetadata.put("name", this.name);
		archiveMetadata.put("recording", this.groupId);
		archiveMetadata.put("backupDate", backupDate);
		archiveMetadata.put("download_url", downloadUrl);
		
		FileIO.writeJSONObject(new File(getRecordingsPath(), 
				getGroupId() + "/" + id + "-archive.json"),
				archiveMetadata);
	}
	
	/**
	 * Tag the recording
	 * 
	 * @param tagType		The kinds of tag (Language, Speaker, OLAC, Custom)
	 * @param tagStr		The contents of the tag
	 * @param userId		The user creating the tag
	 * @return the tag-file name or NULL if it fails
	 * @throws IOException	In case of an issue writing the tag file
	 */
	public String tag(TagType tagType, String tagStr, String userId) throws IOException {
		String tagOwnerId = AikumaSettings.getCurrentUserId();
		if(tagOwnerId == null) {
			Log.e(TAG, "Tagging fails because the creator is not specified");
			return null;
		}
		
		String tagTypeSuffix = "";
		switch(tagType) {
		case LANGUAGE:
			tagTypeSuffix = LANGUAGE_TAG_TYPE;
			languagesBuffer.clear();
			break;
		case SPEAKER:
			tagTypeSuffix = SPEAKER_TAG_TYPE;
			speakersCreatorsBuffer.clear();
			speakersIdsBuffer.clear();
			break;
		case OLAC:
			tagTypeSuffix = OLAC_TAG_TYPE;
			olacTagStringsBuffer.clear();
			break;
		case CUSTOM:
			tagTypeSuffix = CUSTOM_TAG_TYPE;
			customTagStringsBuffer.clear();
			break;
		}

		tagStr = tagStr.trim();
		tagStr = tagStr.replaceAll("\\s+", " ");
		tagStr = tagStr.replace(' ', '_');
		// Tag can only be created for the recordings having the same version with Aikuma version
		File tagFile = new File(FileIO.getOwnerPath(this.versionName, userId), 
				TAG_PATH + getGroupId() + "/" + 
				getId() + "-" + tagTypeSuffix + "-" + tagStr);
		tagFile.getParentFile().mkdirs();
		tagFile.createNewFile();
		
		index(getVersionName() + "-" + getId(), 
				AikumaSettings.getLatestVersion() + "-" + getGroupId() + "-" + tagOwnerId);
		
		return tagFile.getName();
	}
	
	private void filterTags(String tagTypeStr, TagProcessor tagProcessor) {
		List<String> selectedVerGroupOwners = new ArrayList<String>();
		List<File> groupDirList = new ArrayList<File>();
		
		File indexFile = new File(FileIO.getAppRootPath(), "index.json");
		JSONObject indices = null;
		try {
			indices = FileIO.readJSONObject(indexFile);
		} catch (IOException e1) {
			// TODO: How to deal with no index-file?
			Log.e(TAG, "getRespeakings(): error in reading index file");
			indices = new JSONObject();
		}
		
		// Collect directories where related respeakings exist form index file
		JSONArray verGroupOwnerList = (JSONArray) indices.get(getId());
		if(verGroupOwnerList == null) {
			// TODO: How to deal with no index-file?
			verGroupOwnerList = new JSONArray();
		}
		
		for (int i = 0; i < verGroupOwnerList.size(); i++) {
			String verGroupOwnerStr = (String) verGroupOwnerList.get(i);
			String[] splitVerGroupOwnerStr = verGroupOwnerStr.split("-");
			if(!splitVerGroupOwnerStr[0].matches(versionName))	// tag version needs to match recording version
				continue;
			
			File ownerDir = 
					FileIO.getOwnerPath(splitVerGroupOwnerStr[0], splitVerGroupOwnerStr[2]);
			File groupDir = 
					new File(getTagsPath(ownerDir), splitVerGroupOwnerStr[1]);
					
			if(groupDir.exists()) {
				selectedVerGroupOwners.add(verGroupOwnerStr);
				groupDirList.add(groupDir);
			}
				
		}
		
		// Process all the tag files 
		for(int i = 0; i < groupDirList.size(); i++) {
			String verGroupOwnerStr = selectedVerGroupOwners.get(i);
			File groupDir = groupDirList.get(i);
			
			File[] tagFiles = groupDir.listFiles();
			for(File tagFile : tagFiles) {
				String tagTypeSuffix = "";
				String tagStr = "";
				String[] splitName = tagFile.getName().split("-");
				
				if(fileType.equals(SOURCE_TYPE) && 
						splitName[2].equals(SOURCE_TYPE)) {
					tagTypeSuffix = splitName[3];
					tagStr = splitName[4];
				} else if(fileType.equals(splitName[2]) && 
						respeakingId.equals(splitName[3])) {
					tagTypeSuffix = splitName[4];
					tagStr = splitName[5];
				} else
					continue;
				
				if(tagTypeStr.equals(ALL_TAG_TYPE)) {
					tagProcessor.processTag(verGroupOwnerStr, tagFile.getName());
				} else if(tagTypeSuffix.equals(tagTypeStr)) {
					tagProcessor.processTag(verGroupOwnerStr, tagStr);
				}
			}
		}
		
	}

	/**
	 * Tells us whether this phone has already starred the Recording
	 *
	 * @param currentVerName	Current aikuma's version
	 * @param userId			ID of an user who liked this recording
	 * @return	true if a star file with this androidID is present; false
	 * otherwise
	 */
	public boolean isStarredByThisPhone(String currentVerName, String userId) {
		String androidID = Aikuma.getAndroidID();
		File starFile = 
				new File(FileIO.getOwnerPath(currentVerName, userId), "/social/" +
						getId() + "-like");
		return starFile.exists();
	}

	/**
	 * Tells us whether this phone has already flagged the Recording
	 *
	 * @param currentVerName	Current aikuma's version
	 * @param userId			ID of an user who dis-liked this recording
	 * @return	true if a flag file with this androidID is present; false
	 * otherwise
	 */
	public boolean isFlaggedByThisPhone(String currentVerName, String userId) {
		String androidID = Aikuma.getAndroidID();
		File flagFile = 
				new File(FileIO.getOwnerPath(currentVerName, userId), "/social/" +
						getId() + "-report");
		return flagFile.exists();
	}
	
	/**
	 * Tells us whether this recording has been archived
	 *
	 * @return	true if a archiveMetadata file is present; false otherwise
	 */
	public boolean isArchived() {
		File archiveMetaFile = new File(getRecordingsPath(), 
				getGroupId() + "/" + getId() + "-archive.json");
		return archiveMetaFile.exists();
	}

	/**
	 * Gives the number of stars this recording has received.
	 *
	 * @return	The number of stars this recording has recieved
	 */
	public int numStars() {
		// TODO: New file-storage model needs to be decided (1. create index / 2. put these in app-root folder)
		File starDir = new File(FileIO.getOwnerPath(versionName, ownerId), 
				"/social/" +
				getGroupId() + "/" + getId());
		File[] starFiles = starDir.listFiles(
				new FilenameFilter() {
			public boolean accept(File dir, String filename) {
				if (filename.endsWith(".star")) {
					return true;
				}
				return false;
			}
		});
		if (starFiles == null) {
			return 0;
		}
		return starFiles.length;
	}

	/**
	 * Gives the number of flags this recording has received.
	 *
	 * @return	The number of flags this recording has recieved
	 */
	public int numFlags() {
		// TODO: New file-storage model needs to be decided (1. create index / 2. put these in app-root folder)
		File socialDir = new File(FileIO.getOwnerPath(versionName, ownerId), 
				"/social/" +
				getGroupId() + "/" + getId());
		File[] flagFiles = socialDir.listFiles(
				new FilenameFilter() {
			public boolean accept(File dir, String filename) {
				if (filename.endsWith(".flag")) {
					return true;
				}
				return false;
			}
		});
		if (flagFiles == null) {
			return 0;
		}
		return flagFiles.length;
	}

	/**
	 * Gives the number of times this recording has been views.
	 *
	 * @return	The number of times this recording has been viewed.
	 */
	public int numViews() {
		//TODO: New file-storage model for the number of views need to be decided
		File socialDir = new File(FileIO.getOwnerPath(versionName, ownerId),
				"/views/" +
				getGroupId() + "/" + getId());
		File[] flagFiles = socialDir.listFiles();
		if (flagFiles == null) {
			return 0;
		}
		return flagFiles.length;
	}

	/**
	 * Indicates that this recording is allowed to be synced by moving it to a
	 * directory that the SyncUtil synchronizes.
	 *
	 * @return	A transcript of this recording
	 * @param	id	The ID of the recording to sync.
	 * @throws	IOException	If it cannot be moved to the synced directory.
	 */
	/*
	public static void enableSync(String id) throws IOException {
		File wavFile = new File(getNoSyncRecordingsPath(), uuid + ".wav");
		FileUtils.moveFileToDirectory(wavFile, getRecordingsPath(), false);
	}
	*/
	
	/**
	 * Returns the transcript for this recording, or an empty transcript.
	 *
	 * @return	A transcript of this recording
	 */
	public TempTranscript getTranscript() {
		// Find all the transcript files
		File recordingDir = new File(getRecordingsPath(), getGroupId());
		File[] transcriptFiles = recordingDir.listFiles(
				new FilenameFilter() {
			public boolean accept(File dir, String filename) {
				if (filename.split("-")[2].equals("transcript")) {
					Log.i(TAG, "filename: " + filename);
					Log.i(TAG, "split[2]: " +
							filename.split("-")[2]);
					return true;
				}
				return false;
			}
		});

		if(transcriptFiles == null)
			return null;
		
		// Take the first one
		for (File transcriptFile : transcriptFiles) {
			Log.i(TAG, "transcriptFile: " + transcriptFile);
			try {
				return new TempTranscript(this, transcriptFile);
			} catch (IOException e) {
				continue;
			}
		}

		// Just return an empty transcript
		// return new TempTranscript();
		return null;
	}
	
	@Override
	public int describeContents() {
		return 0;
	}

	/**
	 * Creates a Parcel object representing the Recording.
	 *
	 * @param	out	The parcel to be written to
	 * @param	_flags	Unused additional flags about how the object should be
	 * written.
	 */
	public void writeToParcel(Parcel out, int _flags) {
		//TODO: IF needed later
	}
	
	// The temporary UUID of the image before it gets renamed appropriately.
	private UUID imageUUID;
	
	/**
	 * The recording's name.
	 */
	private String name;
	
	/**
	 * The optional free string
	 */
	private String comments;
	
	/**
	 * The recording's date.
	 */
	private Date date;

	private List<Language> languagesBuffer = new ArrayList<Language>();

	private List<String> speakersIdsBuffer = new ArrayList<String>();
	private List<String> speakersCreatorsBuffer = new ArrayList<String>();
	
	private List<String> olacTagStringsBuffer = new ArrayList<String>();
	private List<String> customTagStringsBuffer = new ArrayList<String>();
	
	/**
	 * The device model name
	 */
	private String deviceName;
	
	/**
	 * The Android ID of the device that the recording was made on.
	 */
	private String androidID;

	/**
	 * The ID that represents the group of recordings.
	 */
	private String groupId;

	/**
	 * The sample rate of the recording in Hz
	 */
	private long sampleRate;

	/**
	 * The duration of the recording in seconds (floored)
	 */
	private int durationMsec;

	// The UUID of the source WAV.
	private UUID recordingUUID;

	// The respeaking ID that is at the end of the filename prefix.
	private String respeakingId;

	// The ID of a recording, which is of one of the following structures:
	// (inherited from super-class)
	//
	//		<groupId>-<ownerId>-source (for originals)
	//		<groupId>-<ownerId>-<respeaking type>-<respeakingId> (for
	//		respeakings, transcriptions, commentaries, etc.)
	// private String id;

	//The ID of the source recording.
	private String sourceVerId;

	// Some info regarding the recording format.
	//private String format;
	private int bitsPerSample;
	private int numChannels;
	
	//Location data
	private Double latitude;
	private Double longitude;
	
	/**
	 * Relative path where recording files are stored
	 */
	public static final String PATH = "items/";
	/** Relative path where tag files are stored */
	public static final String TAG_PATH = "social/";
	/** the length of item_id */
	public static final int ITEM_ID_LEN = 16;
	
	/**
	 * Keys of the recording metadata fields
	 */
	public static final String NAME_KEY = "title";
	/** */
	public static final String COMMENTS_KEY = "comments";
	/** */
	public static final String DATE_KEY = "date";
	/** */
	public static final String DEVICE_NAME_KEY = "device";
	/** */
	public static final String ANDROID_ID_KEY = "android_id";
	/** */
	public static final String SAMPLERATE_KEY = "samplerate";
	/** */
	public static final String DURATION_MSEC_KEY = "duration_msec";
	/** */
	public static final String ITEM_ID_KEY = "item_id";
	/** */
	public static final String RESPEAKING_ID_KEY = "suffix";
	/** */
	public static final String SOURCE_VER_ID_KEY = "source";
	/** */
	public static final String LOCATION_KEY = "location";
	/** */
	public static final String FILE_TYPE_KEY = "file_type";
	/** */
	public static final String FORMAT_KEY = "format";
	/** */
	public static final String BITS_PER_SAMPLE_KEY = "bits_per_sample";
	/** */
	public static final String NUM_CHANNELS_KEY = "num_channels";
	/** */
	public static final String CHECKSUM_KEY = "checksum";
	
	/** 
	 * Keys used/tweaked in cloud fullText metadata fields
	 */
	public static final String ITEM_ID_PREFIX_KEY = "item_pre";
	/** */
	public static final String DURATION_CLOUD_KEY = "dur";
	/** 4 range of durations (used for fullText-indesing/search) */
	public enum DurRange {
		/** S: < 3min, M: 3-10min, L:10-30min, XL: > 30min */
		S(-2), M(-3), L(-4), X(-5);
		private final int value;
		private DurRange(int val) {this.value = val; }
		/** @return the integer represenation value   */
		public int getValue() { return this.value; }
		/** @return min integer representation value(S:-2) */
		public static int getMinValue() { return DurRange.S.value; }
		/** @return max integer representation value(XL:-5) */
		public static int getMaxValue() { return DurRange.X.value; }
		/** Factory function creating DurRange from miliSeconds 
		 *  @param	durMsec	MiliSeconds
		 *  @return	DurRange instance
		 */
		public static DurRange fromDurationMsec(int durMsec) {
			if(durMsec > 1800000) {
				return DurRange.X;
			} else if(durMsec > 600000) {
				return DurRange.L;
			} else if(durMsec > 180000) {
				return DurRange.M;
			} else if(durMsec >= 0) {
				return DurRange.S;
			} else {
				return null;
			}
		}
	}
	
	private static Set<String> fieldKeySet;
	static {
		fieldKeySet = new HashSet<String>();
		fieldKeySet.add(NAME_KEY);
		fieldKeySet.add(COMMENTS_KEY);
		fieldKeySet.add(DATE_KEY);
		fieldKeySet.add(VERSION_KEY);
		fieldKeySet.add(USER_ID_KEY);
		fieldKeySet.add(DEVICE_NAME_KEY);
		fieldKeySet.add(ANDROID_ID_KEY);
		fieldKeySet.add(SAMPLERATE_KEY);
		fieldKeySet.add(DURATION_MSEC_KEY);
		fieldKeySet.add(ITEM_ID_KEY);
		fieldKeySet.add(RESPEAKING_ID_KEY);
		fieldKeySet.add(SOURCE_VER_ID_KEY);
		fieldKeySet.add(LOCATION_KEY);
		fieldKeySet.add(FILE_TYPE_KEY);
		fieldKeySet.add(FORMAT_KEY);
		fieldKeySet.add(BITS_PER_SAMPLE_KEY);
		fieldKeySet.add(NUM_CHANNELS_KEY);
	}
	
	/** A list of tag types */
	public enum TagType { LANGUAGE, SPEAKER, OLAC, CUSTOM };
	/** */
	public static final String LANGUAGE_TAG_TYPE = "language";
	/** */
	public static final String SPEAKER_TAG_TYPE = "speaker";
	/** */
	public static final String OLAC_TAG_TYPE = "olac";
	/** */
	public static final String CUSTOM_TAG_TYPE = "custom";
	/** */
	public static final String ALL_TAG_TYPE = "alltags";
	
	private static Set<String> tagTypeSet;
	static {
		tagTypeSet = new HashSet<String>();
		tagTypeSet.add(LANGUAGE_TAG_TYPE);
		tagTypeSet.add(SPEAKER_TAG_TYPE);
		tagTypeSet.add(OLAC_TAG_TYPE);
		tagTypeSet.add(CUSTOM_TAG_TYPE);	// The filename has the format(speakerID-image-small)
	}
	
	
	/**
	 * A function pointer passed to filterTags()
	 */
	private interface TagProcessor {
		/**
		 * Interface function to process collected tags
		 * @param verGroupOwnerStr	a string of (version)-(Group ID)-(Owner ID)
		 * @param tagContentStr		a string of tag content
		 */
		public void processTag(String verGroupOwnerStr, String tagContentStr);
	}
} public abstract class Player {

	/**
	 * The class that is to be implemented that is given to a Player and has
	 * it's onCompletion method run when the player completes playing a
	 * recording.
	 */
	public static abstract class OnCompletionListener {

		/**
		 * The method called when the player gets to completion.
		 *
		 * @param	player	The player
		 */
		public abstract void onCompletion(Player player);
	}

	/**
	 * Set the callback to be run when the recording completes playing.
	 *
	 * @param	listener	The callback to be invoked
	 */
	public abstract void setOnCompletionListener(OnCompletionListener listener);

	/**
	 * Get the duration of the recording in milliseconds.
	 *
	 * @return	The duration of the recording in milliseconds.
	 **/
	public abstract int getDurationMsec();

	/**
	 * Seek to a given point in the recording in milliseconds.
	 *
	 * @param	msec	The point in the recording to seek to (milliseconds).
	 */
	public abstract void seekToMsec(int msec);

	/**
	 * Indicates whether the recording is currently being played.
	 *
	 * @return	true if recording is being played; false otherwise.
	 */
	public abstract boolean isPlaying();

	/** Pauses the playback. */
	public abstract void pause();

	/** Starts or resumes playback of the recording. */
	public abstract void play();

	/**
	 * Get current point in the recording in milliseconds.
	 *
	 * @return	The current position in milliseconds as an integer.
	 */
	public abstract int getCurrentMsec();

	/**
	 * Releases resources associated with the player
	 */
	public abstract void release();

	/**
	 * Converts a sample to milliseconds.
	 *
	 * @param	sample	The sample.
	 * @return	The milliseconds as an integer.
	 */
	public abstract int sampleToMsec(long sample);

} public class SimplePlayer extends Player implements Sampler {

	/**
	 * Creates a player to play the supplied recording.
	 *
	 * @param	recording	The metadata of the recording to play.
	 * @param	playThroughSpeaker	True if the audio is to be played through the main
	 * speaker; false if through the ear piece (ie the private phone call style)
	 * @throws	IOException	If there is an issue reading the audio source.
	 */
	public SimplePlayer(Recording recording, boolean playThroughSpeaker)
			throws IOException {
		setRecording(recording);
		mediaPlayer = new MediaPlayer();
		if (playThroughSpeaker) {
			mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
		} else {
			mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
		}
		mediaPlayer.setDataSource(recording.getFile().getCanonicalPath());
		mediaPlayer.prepare();
		setSampleRate(recording.getSampleRate());
	}

	private void setRecording(Recording recording) {
		this.recording = recording;
	}

	/**
	 * Creates a player to play the supplied recording for when no Recording
	 * metadata file exists
	 *
	 * @param	recordingFile	The location of the recording as a File
	 * @param	sampleRate	The sample rate of the recording
	 * @param	playThroughSpeaker	True if the audio is to be played through the main
	 * speaker; false if through the ear piece (ie the private phone call style)
	 * @throws	IOException	If there is an issue reading the audio source.
	 */
	public SimplePlayer(File recordingFile, long sampleRate,
			boolean playThroughSpeaker) throws IOException {
		mediaPlayer = new MediaPlayer();
		if (playThroughSpeaker) {
			mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
		} else {
			mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
		}
		mediaPlayer.setDataSource(recordingFile.getCanonicalPath());
		mediaPlayer.prepare();
		setSampleRate(sampleRate);
	}

	/** Starts or resumes playback of the recording. */
	public void play() {
		this.finishedPlaying = false;
		mediaPlayer.start();
	}

	/** Pauses the playback. */
	public void pause() {
		try {
			// If it's not in a started state, then the OnCompletionListener
			// may be called when pause() is, so ensure it's already playing
			// before calling pause().
			if (mediaPlayer.isPlaying()) {
				mediaPlayer.pause();
			}
		} catch (IllegalStateException e) {
			//If it's in an illegal state, then it wouldn't be playing anyway,
			//so no issue.
		}
	}
	
	/**
	 * Rewind back for rewindAmount
	 * @param rewindAmount	in msec
	 */
	public void rewind(int rewindAmount) {
		int currentPos = getCurrentMsec();
		int rewindedPos = (currentPos-rewindAmount);
		rewindedPos = rewindedPos < 0 ? 0 : rewindedPos;
		seekToMsec(rewindedPos);
	}

	/**
	 * Indicates whether the recording is currently being played.
	 *
	 * @return	true if the player is currently playing; false otherwise.
	 */
	public boolean isPlaying() {
		return mediaPlayer.isPlaying();
	}

	/**
	 * Get current point in the recording in milliseconds.
	 *
	 * @return	The current point in the recording in milliseconds as an int.
	 */
	public int getCurrentMsec() {
		try {
			return mediaPlayer.getCurrentPosition();
		} catch (IllegalStateException e) {
			//If we get an IllegalStateException because the recording has
			//finished playing, just return the duration of the recording.
			return getDurationMsec();
		}
	}

	public long getCurrentSample() {
		return msecToSample(getCurrentMsec());
	}

	/**
	 * Get the duration of the recording in milliseconds.
	 *
	 * @return	The duration of the audio in milliseconds as an int.
	 */
	public int getDurationMsec() {
		try {
			return mediaPlayer.getDuration();
		} catch (IllegalStateException e) {
			//If this fails then we won't be in an activity where a duration is
			//actually required. -1 is returned so we at least know it was
			//erroneous if we ever need to.
			return -1;
		}
	}

	/**
	 * Seek to a given point in the recording in milliseconds.
	 *
	 * @param	msec	The time to jump the playback to in milliseconds.
	 */
	public void seekToMsec(int msec) {
		mediaPlayer.seekTo(msec);
	}

	/**
	 * Moves the recording to the given sample.
	 *
	 * @param	sample	The sample to jump playback to.
	 */
	public void seekToSample(long sample) {
		seekToMsec(sampleToMsec(sample));
	}

	/** Releases the resources associated with the SimplePlayer */
	public void release() {
		mediaPlayer.release();
	}

	/**
	 * Set the callback to be run when the recording completes playing.
	 *
	 * @param	listener	The callback to be called when the recording
	 * complets playing.
	 */
	public void setOnCompletionListener(final OnCompletionListener listener) {
		mediaPlayer.setOnCompletionListener(
				new MediaPlayer.OnCompletionListener() {
					public void onCompletion(MediaPlayer _mp) {
						listener.onCompletion(SimplePlayer.this);
						try {
							if(recording != null)
								incrementViewCount();
						} catch (IOException e) {
							// There is likely an issue writing to the
							// filesystem, but it's probably not worth
							// reporting to the user with at Toast.
						}
						SimplePlayer.this.finishedPlaying = true;
					}
				});
	}

	/**
	 * Increment the view count by adding a view file to the Recording's view
	 * directory.
	 */
	private void incrementViewCount() throws IOException {
		String versionName = recording.getVersionName();
		String ownerId = recording.getOwnerId();
		File viewDir = new File(FileIO.getOwnerPath(versionName, ownerId), 
				"views/" +
				recording.getGroupId() + "/" + recording.getId());
		viewDir.mkdirs();

		File viewFile = new File(viewDir, UUID.randomUUID() + ".view");
		viewFile.createNewFile();
	}

	/**
	 * Returns the sample rate of this recording
	 *
	 * @return	The sample rate of this recording.
	 */
	public long getSampleRate() {
		//If the sample rate is less than zero, then this indicates that there
		//wasn't a sample rate found in the metadata file.
		if (sampleRate <= 0l) {
			throw new RuntimeException(
					"The sampleRate of the recording is not known.");
		}
		return sampleRate;
	}

	private void setSampleRate(long sampleRate) {
		if (sampleRate == -1)
			this.sampleRate = AikumaSettings.DEFAULT_SAMPLERATE;
		else
			this.sampleRate = sampleRate;
	}

	/**
	 * Converts a value of samples into milliseconds assuming this recording's
	 * sample rate.
	 *
	 * @param	sample	sample value to be converted.
	 * @return	A millisecond value as an integer.
	 */
	public int sampleToMsec(long sample) {
		long msec = sample / (getSampleRate() / 1000);
		if (msec > Integer.MAX_VALUE) {
			return Integer.MAX_VALUE;
		}
		return (int) msec;
	}

	/**
	 * Converts a millisecond value into samples assuming this recording's
	 * sample rate.
	 *
	 * @param	msec	A time value in milliseconds.
	 * @return	A sample value as a long.
	 */
	public long msecToSample(int msec) {
		return msec * (getSampleRate() / 1000);
	}

	public boolean isFinishedPlaying() {
		return this.finishedPlaying;
	}

	/** The MediaPlayer used to play the recording. **/
	private MediaPlayer mediaPlayer;
	private long sampleRate;
	private boolean finishedPlaying;
	private Recording recording;
} public class InterleavedPlayer extends Player {
	
	/**
	 * Creates an InterleavedPlayer to play the supplied recording and it's
	 * original
	 *
	 * @param	context		The android-context creating this player for beep-audio
	 * @param	recording	The metadata of the recording to play.
	 * @throws	IOException	If there is an issue reading the recordings.
	 */
	public InterleavedPlayer(Context context, Recording recording) throws IOException {
		setRecording(recording);
		if (recording.isOriginal()) {
			throw new IllegalArgumentException("The supplied Recording is " +
					"not a respeaking. Use SimplePlayer instead.");
		}
		// Initialize original recording player
		Recording originalRecording = recording.getOriginal();
		setSampleRate(originalRecording.getSampleRate());
		original = new MarkedPlayer(
				originalRecording,
				new OriginalMarkerReachedListener(), true);
		
		// Initialize segment/derivative player
		if(context != null && recording.getFileType().equals(Recording.SEGMENT_TYPE)) {	//segment
			beeper = new Beeper(context, new MediaPlayer.OnCompletionListener() {
				@Override
				public void onCompletion(MediaPlayer mp) {
					advanceOriginalSegment();
					playOriginal();
					if(currentOriginalSegment.getEndSample() == Long.MAX_VALUE)
						completedOnce = true;
				}
			});
		} else {	//derivative(respeak/interpret)
			respeaking = new MarkedPlayer(recording,
					new RespeakingMarkerReachedListener(), true);
		}
		segments = new Segments(recording);
		initializeCompletionListeners();
		completedOnce = false;
	}

	// Initializes the completion listeners for the original and respeaking,
	// such that when one of them completes the InterleavedPlayer's
	// onCompletionListener is run, only once.
	// ex. Segment: Segment -> onCompletion
	// 	   Derivative: Original(completedOnce) -> Derivative -> onCompletion
	private void initializeCompletionListeners() {
		Player.OnCompletionListener bothCompletedListener = new 
				Player.OnCompletionListener() {
			//boolean completedOnce = false;
			@Override
			public void onCompletion(Player p) {
				if (respeaking == null || completedOnce) {
					if (onCompletionListener != null) {
						onCompletionListener.onCompletion(p);
					}
					reset();
					completedOnce = false;
				} else {
					completedOnce = true;
				}
			}
		};
		original.setOnCompletionListener(bothCompletedListener);
		if(respeaking != null)
			respeaking.setOnCompletionListener(bothCompletedListener);
	}

	/**
	 * Resumes playing the interleaved recording, from the original segment
	 * where it was last paused.
	 */
	public void play() {
		playOriginal();
	}

	/**
	 * Resets the player to the beginning.
	 */
	public void reset() {
		originalSegmentIterator = null;
		currentOriginalSegment = null;
		original.seekToSample(0l);
		if(respeaking != null)
			respeaking.seekToSample(0l);
	}

	/**
	 * Indicates whether the recording is currently being played.
	 *
	 * @return	true if the recording is currently playing; false otherwise.
	 */
	public boolean isPlaying() {
		return original.isPlaying() || (respeaking != null && respeaking.isPlaying());
	}

	/** Pauses the playback. */
	public void pause() {
		if (original.isPlaying()) {
			original.pause();
		}
		if (respeaking != null && respeaking.isPlaying()) {
			respeaking.pause();
		}
	}

	/**
	 * Get current point in the recording in milliseconds.
	 *
	 * @return	The current point in the recording in milliseconds as an int.
	 */
	public int getCurrentMsec() {
		return original.getCurrentMsec();
	}

	/**
	 * Get the duration of the recording in milliseconds.
	 *
	 * @return	The duration of the recording in milliseconds as an int.
	 */
	public int getDurationMsec() {
		return original.getDurationMsec();
	}

	/** Releases resources associated with the InterleavedPlayer. */
	public void release() {
		original.release();
		if(respeaking != null)
			respeaking.release();
	}

	// Plays the current original segment.
	private void playOriginal() {
		playSegment(getCurrentOriginalSegment(), original);
	}

	private Segment getCurrentOriginalSegment() {
		if (currentOriginalSegment == null) {
			advanceOriginalSegment();
		}
		return currentOriginalSegment;
	}

	// Plays the current respeaking segment.
	private void playRespeaking() {
		if(respeaking != null)
			playSegment(getCurrentRespeakingSegment(), respeaking);
	}

	private Segment getCurrentRespeakingSegment() {
		return segments.getRespeakingSegment(getCurrentOriginalSegment());
	}

	// Plays the specified segment in the specified player.
	private void playSegment(Segment segment, MarkedPlayer player) {
		if (segment != null) {
			player.seekTo(segment);
			player.setNotificationMarkerPosition(segment);
			player.play();
		}
	}

	// Moves forward to the next original segment in the recording.
	private void advanceOriginalSegment() {
		if (getOriginalSegmentIterator().hasNext()) {
			currentOriginalSegment = getOriginalSegmentIterator().next();
		} else {
			long startSample;
			if (currentOriginalSegment != null) {
				startSample = currentOriginalSegment.getEndSample();
			} else {
				startSample = 0l;
			}
			currentOriginalSegment = new Segment(startSample, Long.MAX_VALUE);
		}
	 }

	// Gets an iterator over the segments of the original recording.
	private Iterator<Segment> getOriginalSegmentIterator() {
		if (originalSegmentIterator == null) {
			originalSegmentIterator = segments.getOriginalSegmentIterator();
		}
		return originalSegmentIterator;
	}

	private MarkedPlayer original;
	private MarkedPlayer respeaking;
	private Segments segments;
	private Segment currentOriginalSegment;
	private Iterator<Segment> originalSegmentIterator;
	private Player.OnCompletionListener onCompletionListener;
	private Recording recording;
	private Beeper beeper;

	private void setRecording(Recording recording) {
		this.recording = recording;
	}

	public Recording getRecording() {
		return this.recording;
	}

	/**
	 * Implements a method for the original audio source to call when an
	 * original segment has finished playing.
	 */
	private class OriginalMarkerReachedListener extends
			MarkedPlayer.OnMarkerReachedListener {
		public void onMarkerReached(MarkedPlayer p) {
			Log.i("release", "original onMarker reached, completedOnce = " +
					completedOnce);
			original.pause();
			if(respeaking != null)
				playRespeaking();
			else {
				if (!completedOnce) {
					if(beeper != null)
						beeper.beep();
					else {
						advanceOriginalSegment();
						playOriginal();
						if(currentOriginalSegment.getEndSample() == Long.MAX_VALUE)
							completedOnce = true;
					}
				}
			}
		}
	}

	/**
	 * Implements a method for the respeaking audio source to call when a
	 * respeaking segment has finished playing.
	 */
	private class RespeakingMarkerReachedListener extends
			MarkedPlayer.OnMarkerReachedListener {
		public void onMarkerReached(MarkedPlayer p) {
			Log.i("release", "respeaking onMarker reached, completedOnce = " +
					completedOnce);
			respeaking.pause();
			if (!completedOnce) {
				advanceOriginalSegment();
				playOriginal();
			}
		}
	}

	/**
	 * Gets the sample rate of this player.
	 *
	 * @return	The sample rate of the player as a long.
	 */
	public long getSampleRate() {
		//If the sample rate is less than zero, then this indicates that there
		//wasn't a sample rate found in the metadata file.
		if (sampleRate <= 0l) {
			throw new RuntimeException(
					"The sampleRate of the recording is not known.");
		}
		return sampleRate;
	}

	private void setSampleRate(long sampleRate) {
		this.sampleRate = sampleRate;
	}

	/**
	 * Converts samples to milliseconds assuming this players sample rate
	 *
	 * @param	sample	A sample value to be converted
	 * @return	The value in milliseconds as an int.
	 */
	public int sampleToMsec(long sample) {
		long msec = sample / (getSampleRate() / 1000);
		if (msec > Integer.MAX_VALUE) {
			return Integer.MAX_VALUE;
		}
		return (int) msec;
	}

	/**
	 * Seek to a given point in the recording in milliseconds.
	 *
	 * @param	msec	The point in the recording to seek to in milliseconds.
	 */
	public void seekToMsec(int msec) {
		reset();
		while (getOriginalSegmentIterator().hasNext()) {
			currentOriginalSegment = getOriginalSegmentIterator().next();
			int start = sampleToMsec(currentOriginalSegment.getStartSample());
			int end = sampleToMsec(currentOriginalSegment.getEndSample());
			//Log.i("interleaved", msec + " / " + this.getDurationMsec() + ": (" + start + ", " + end + ")");
			if (msec >= start && msec <= end)
				break;			
		}
	}

	/**
	 * Set the callback to be run when the recording completes playing.
	 *
	 * @param	listener	The callback to call when playing is complete.
	 */
	public void setOnCompletionListener(
			final Player.OnCompletionListener listener) {
		this.onCompletionListener = listener;
	}

	private static boolean completedOnce = false;
	private long sampleRate;
} public class MarkedPlayer extends SimplePlayer {

	/**
	 * Constructor
	 *
	 * @param	recording	The metadata of the recording to be played.
	 * @param	listener	The callback to call when a marker is reached.
	 * @param	playThroughSpeaker	True if the audio is to be played through the main
	 * speaker; false if through the ear piece (ie the private phone call style)
	 * @throws	IOException	If there is an issue reading from the data source.
	 */
	public MarkedPlayer(Recording recording, OnMarkerReachedListener listener,
			boolean playThroughSpeaker) throws IOException {
		super(recording, playThroughSpeaker);
		setOnMarkerReachedListener(listener);
		notificationMarkerLoop = new Thread(new NotificationMarkerLoop());
		notificationMarkerLoop.start();
		unsetNotificationMarkerPosition();
		count++;
	}

	/**
	 * Constructor
	 *
	 * @param	recording	The metadata of the recording to be played
	 * @throws	IOException	If an I/O issue occurs in the superconstructor.
	 * @param	playThroughSpeaker	True if the audio is to be played through the main
	 * speaker; false if through the ear piece (ie the private phone call style)
	 */
	public MarkedPlayer(Recording recording, boolean playThroughSpeaker) throws IOException {
		super(recording, true);
		notificationMarkerLoop = new Thread(new NotificationMarkerLoop());
		notificationMarkerLoop.start();
		unsetNotificationMarkerPosition();
		count++;
	}

	/**
	 * Returns the marker position in milliseconds
	 *
	 * @return	marker position in milliseconds
	 */
	public int getNotificationMarkerPositionMsec() {
		return notificationMarkerPosition;
	}

	/**
	 * Sets the position of the notification marker.
	 *
	 * @param	notificationMarkerPosition	marker in milliseconds
	 */
	public void setNotificationMarkerPositionMsec(
			int notificationMarkerPosition) {
		this.notificationMarkerPosition = notificationMarkerPosition;
	}

	/**
	 * Sets the notification marker to be at the end of the supplied segment
	 *
	 * @param	segment	The segment whose end is to be marked.
	 */
	public void setNotificationMarkerPosition(Segment segment) {
		if (segment != null) {
			setNotificationMarkerPositionMsec(
					sampleToMsec(segment.getEndSample()));
		} else {
			unsetNotificationMarkerPosition();
		}
	}

	/**
	 * Sets the notificaiton marker position to be at the specified sample.
	 *
	 * @param	sample	The sample at which the notification marker is to be
	 * set.
	 */
	public void setNotificationMarkerPositionSample(Long sample) {
		setNotificationMarkerPositionMsec(sampleToMsec(sample));
	}

	/**
	 * Removes the marker if it has been set.
	 */
	public void unsetNotificationMarkerPosition() {
		setNotificationMarkerPositionMsec(-1);
	}

	@Override
	public void release() {
		super.release();
		stopNotificationMarkerLoop();
	}

	/**
	 * Seeks to the beginning of the given segment.
	 *
	 * @param	segment	The segment to seek to.
	 */
	public void seekTo(Segment segment) {
		super.seekToSample(segment.getStartSample());
	}

	/**
	 * The class for listeners what would be called when set markers get
	 * reached.
	 */
	public static abstract class OnMarkerReachedListener {
		/**
		 * The method to be called when the set marker is reached.
		 *
		 * @param	p	The marked player whose marker has been reached.
		 */
		public abstract void onMarkerReached(MarkedPlayer p);
	}

	@Override
	public void setOnCompletionListener(final OnCompletionListener listener) {
		super.setOnCompletionListener(
				new Player.OnCompletionListener() {
					public void onCompletion(Player _p) {
						if(MarkedPlayer.this.onMarkerReachedListener != null) {
							MarkedPlayer.this.onMarkerReachedListener.
							onMarkerReached(MarkedPlayer.this);
						}
						listener.onCompletion(MarkedPlayer.this);
					}
				});
	}


	/**
	 * Sets the listener the MarkedPlayer notifies when a previously set
	 * marker is reached.
	 *
	 * @param	onMarkerReachedListener	the listener to be notified.
	 */
	public void setOnMarkerReachedListener(OnMarkerReachedListener
			onMarkerReachedListener) {
		this.onMarkerReachedListener = onMarkerReachedListener;
	}

	/** The listener that is notified when a set marker is reached */
	private OnMarkerReachedListener onMarkerReachedListener;

	// Stops the notification marker loop.
	private void stopNotificationMarkerLoop() {
		if (notificationMarkerLoop != null) {
			notificationMarkerLoop.interrupt();
		}
	}

	private static int count = 0;

	/**
	 * Implements a run that polls the current position to check
	 * if a previously set marker has been reached
	 */
	private class NotificationMarkerLoop implements Runnable {
		public void run() {
			while (true) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// The player is being released so this thread should end.
					break;
				}
				/* For later debugging 
				 * (This is commented out because of too many logs)
				Log.i(TAG, "notification marker position msec: " +
						getNotificationMarkerPositionMsec() +
						"\ngetCurentMsec(): " + getCurrentMsec());
				
				Log.i(TAG, "notification marker position sample: " +
						msecToSample(getNotificationMarkerPositionMsec()) +
						"\ngetCurentMsec() as sample: " +
						msecToSample(getCurrentMsec()));
				 */
				if (notificationMarkerPosition >= 0) {
					if (getCurrentMsec() >=
							getNotificationMarkerPositionMsec()) {
						onMarkerReachedListener.onMarkerReached(
								MarkedPlayer.this);
						unsetNotificationMarkerPosition();
					}
				}
			}
		}
	}

	/**
	 * The marker that determines when the playbackPositionUpdateListener gets
	 * called.
	 */
	private int notificationMarkerPosition;

	/**
	 * The thread that loops to check if markers have been reached.
	 */
	private Thread notificationMarkerLoop;

	private static final String TAG = "MarkedPlayer";

	//////////////////////////////////////////////////////////////////////////


	/*
	private boolean hasStartedPlaying = false;
	
	public void start() {
		//stopNotificationMarkerLoop();
		startNotificationMarkerLoop();
		mediaPlayer.start();
		if (!hasStartedPlaying) { hasStartedPlaying = true; }
	}
	
	public void pause() {
		if (hasStartedPlaying) {
			stopNotificationMarkerLoop();
			mediaPlayer.pause();
		}
	}

	public void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) {
		mediaPlayer.setOnPreparedListener(listener);
	}

	private void unsetNotificationMarkerPosition() {
		setNotificationMarkerPosition(-1);
	}
	
	private void startNotificationMarkerLoop() {
		notificationMarkerLoop = new Thread(
				new NotificationMarkerLoop(),
				"NotificationMarkerLoop");
		notificationMarkerLoop.start();
	}

	private void stopNotificationMarkerLoop() {
		if (notificationMarkerLoop != null) {
			notificationMarkerLoop.interrupt();
			//If the thread is still alive, wait. This method should block
			//until the job is done.
			//while (notificationMarkerLoop.isAlive()) {}
		}
	}
	*/
} public class TranscriptPlayer extends MarkedPlayer {

	/**
	 * Constructor
	 *
	 * @param	recording	The recording to be played
	 * @param	activity	The activity to modify as the transcriptions
	 * change.
	 * @throws	IOException	If there is an issue reading transcriptions or the
	 * recording
	 */
	public TranscriptPlayer(Recording recording, final Activity activity)
			throws IOException {
		super(recording, true);

		this.activity = activity;

		transcript = recording.getTranscript();

		if(transcript != null) {
			updateTranscriptStatus(getCurrentSample());

			OnMarkerReachedListener onTranscriptMarkerReachedListener =
					new OnMarkerReachedListener() {
				public void onMarkerReached(MarkedPlayer p) {
					activity.runOnUiThread(new Runnable() {
						public void run() {
							updateTranscriptStatus(getCurrentSample());
						}
					});
				}
			};

			setOnMarkerReachedListener(onTranscriptMarkerReachedListener);
		}
		
	}

	@Override
	public void play() {
		super.play();
		if(transcript != null) {
			updateTranscriptStatus(getCurrentSample());
		}
	}

	// Updates the transcript Ui and prepares the notification marker position.
	private void updateTranscriptStatus(long sample) {
		Segment segment = transcript.getSegmentOfSample(sample);
		updateTranscriptUi(segment);
		setNotificationMarkerPosition(segment);
	}

	// Update the UI such that the transcript reflects the current sample.
	private void updateTranscriptUi(long sample) {
		Segment segment = transcript.getSegmentOfSample(sample);
		updateTranscriptUi(segment);
	}

	// Update the UI such that the transcript reflects the current segment.
	private void updateTranscriptUi(Segment segment) {
		transcriptView = (TextView)
			activity.findViewById(R.id.transcriptView);
		transcriptView2 = (TextView)
			activity.findViewById(R.id.transcriptView2);
		if (segment != null) {
			transcriptView.setText(
					transcript.getTranscriptPair(segment).transcript);
			transcriptView2.setText(
					transcript.getTranscriptPair(segment).translation);
		} else {
			transcriptView.setText("");
		}
	}

	@Override
	public void seekToMsec(int msec) {
		super.seekToMsec(msec);

		//Find the transcript segment that corresponds to this point in time
		long sample = msecToSample(msec);

		//Update the UI to reflect the current transcript.
		updateTranscriptStatus(sample);
	}

	
	/*
	/**
	 * Listener to adjust the transcript view when a transcript marker gets reached.
	private class TranscriptMarkerReachedListener
			extends MarkedPlayer.OnMarkerReachedListener {
		public void onMarkerReached(MarkedPlayer p) {
			transcriptView.setText("different");
		}
	}
	*/

	private static Segment segment;
	private static TextView transcriptView;
	private static TextView transcriptView2;
	private TempTranscript transcript;
	private Activity activity;
} public class ProximityDetector {

	private static final String TAG = ProximityDetector.class.getSimpleName();
	
	/**
	 * The sensor manager that provides the readings.
	 */
	protected SensorManager sensorManager;

	/**
	 * The threshold distance in centimeters that determines whether a users
	 * face is near or far from the phone.
	 */
	protected float threshold;
	/**
	 * True if users face is within the threshold; false otherwise.
	 */
	protected boolean close;

	/**
	 * The listener that provides the functionality when sensor readings change.
	 */
	protected final SensorEventListener sensorListener =
			new SensorEventListener() {

		public void onSensorChanged(SensorEvent sensorEvent) {
			float distance = sensorEvent.values[0];
			Log.i(TAG, "close: " + close + ", threshold: " + distance);
			if (!close && distance <= threshold) {
				near(distance);
				close = true; // report only once
			}
			if (close && distance > threshold) {
				far(distance);
				close = false; // report only once
			}
		}

		public void onAccuracyChanged(Sensor sensor, int accuracy) {
			// We ignore a changing accuracy for now.
			}
	};

	/**
	 * Constructor that defaults to a threshold distance of 2 centimeters.
	 *
	 * @param	activity	The activity the proximity sensor belongs to.
	 */
	public ProximityDetector(Activity activity) {
		this(activity, 2.0f);
	}

	/**
	 * Constructor that allows for specification of the threshold.
	 *
	 * @param	activity	The activity the proximity sensor belongs to.
	 * @param	threshold	The distance in centimeters that is the threshold
	 * that determines whether the user's face is near or far.
	 */
	public ProximityDetector(Activity activity, float threshold) {
		this.sensorManager = (SensorManager)
				activity.getSystemService(Context.SENSOR_SERVICE);
		this.threshold = threshold;
	}

	/**
	 * Returns true if the users face is close to the phone.
	 *
	 * @return	true if the user is close; false otherwise.
	 */
	public boolean isNear() {
		return close;
	}

	/**
	 * Override this method to describe what should happen when the users face
	 * becomes close.
	 *
	 * @param	distance	The distance in centimeters between the users face
	 * and the proximity detector.
	 */
	public void near(float distance) {};

	/**
	 * Override this method to describe what should happen when the users face
	 * becomes far.
	 *
	 * @param	distance	The distance in centimeters between the users face
	 * and the proximity detector.
	 */
	public void far(float distance) {};

	/**
	 * Start the proximity detection.
	 */
	public void start() {
		this.close = false;
		boolean success = sensorManager.registerListener(
				sensorListener,
				sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY),
				SensorManager.SENSOR_DELAY_NORMAL
		);
		Log.i(TAG, "registered: " + success);
	}

	/**
	 * Stop the proximity detection; do not forget to call this.
	 */
	public void stop() {
		sensorManager.unregisterListener(sensorListener);
	}

} public class AikumaSettings {
	/**
	 * Latest version name.
	 * 
	 * v02: 1. Recording-metadata: delete languages/speakers, add comments
	 * 		2. Speaker-metadata: delete languages, add comments/date
	 * 		3. Delete speaker-images
	 * 		4. Add recording-images
	 * 		5. New files: Tags (speaker, language, discourse-type, custom)
	 *
	 *      1. translation -> interpret, resepaking -> respeak
	 * 		2. mapping, transcript : TXT -> JSON
	 * 		3. deletion, archive files (?)
	 * 
	 * v03: 1. Flat file structure in cloud-storage
	 * 		2. Use of aikuma-cloud-library-0.10.1
	 * 		3. Remove two levels of hashes in local storage
	 * 		4. Admin-file structure in local storage
	 * 		5. Checksum field for recording
	 */
	private static final String DEFAULT_VERSION = "v03";	
	
	/**
	 * Setting value for backup
	 */
	public static boolean isBackupEnabled = true;
	/**
	 * Setting value for auto-download
	 */
	public static boolean isAutoDownloadEnabled = true;
	/**
	 * Setting value for sync-only-over-cellulars
	 */
	public static boolean isOnlyWifi;
	/**
	 * Setting value for sync-over-wifi
	 */
	public static boolean isNetwork;
	/**
	 * Setting value for public-share
	 */
	public static boolean isPublicShareEnabled;
	/**
	 * State value for cloud search
	 */
	public static boolean isSearchPossible;
	
	// Current default owner_id(== default Google account)
	private static String DEFAULT_USER_ID = null;
	// Current default owner_auth_token(== default Google API access_token)
	private static String DEFAULT_USER_AUTH_TOKEN = null;
	private static String DEFAULT_USER_ID_TOKEN = null;
	private static String DEFAULT_INDEX_SERVER = "https://aikuma.info:443/index";
	private static String DEFAULT_FILE_PROXY_SERVER = "https://aikuma.info:443";
	private static String DEFAULT_INDEX_SERVER_CLIENT_ID = "530026557211-46brcqr1p4ltlru6j6duc0am14oh83ei.apps.googleusercontent.com";
	/**
	 * Account of central fusion table
	 */
	public static String CENTRAL_USER_ID = "lp20.org@gmail.com";
	/**
	 * Temporary ID for all root-folder (TODO: might be changed depending on project folder structures)
	 */
	public static String ROOT_FOLDER_ID = "aikuma-" + DEFAULT_VERSION;
	
	private static int numOfUsers;
	private static int numOfSpeakers;
	private static int numOfItems;
	private static float ratioOfFtpSync;
	private static float ratioOfCloudSync;
	private static float ratioOfCentralSync;
	
	
	/**
	 * Key for a versionName in default SharedPreferences
	 */
	public static final String SETTING_VERSION_KEY = "version";
	/**
	 * Key for a current user ID in default SharedPreferences
	 */
	public static final String SETTING_OWNER_ID_KEY = "ownersID";
	/** 
	 * Key for a current user ID's auth_token in default SharedPreferences
	 */
	public static final String SETTING_AUTH_TOKEN_KEY = "userToken";
	/**
	 * Key for current user ID's google ID token in default SharedPreferences
	 */
	public static final String SETTING_ID_TOKEN_KEY = "userIdToken";
	/**
	 * Key for a ratio of backup to Google Drive in default SharedPreferences
	 */
	public static final String SETTING_CLOUD_RATIO_KEY = "cloudSyncRatio";
	/**
	 * Key for a ratio of central-archive in default SharedPreferences
	 */
	public static final String SETTING_CENTRAL_RATIO_KEY = "centralSyncRatio";
	/**
	 * Key for index server url
	 */
	public static final String SETTING_INDEX_SERVER_URL = "indexServerUrl";
	
	/**
	 * Sync time interval (30min)
	 */
	public static final long SYNC_INTERVAL = 30 * 60 * 1000;
	
	/**
	 * Extra audio recording duration after a thumb is lifted or head becomes far away
	 */
	public static final int EXTRA_AUDIO_DURATION = 250;
	
	/**
	 * Default rewind time when creating a derivative recording (in msec)
	 */
	public static final int DEFAULT_RESPEAK_REWIND = 300;
	
	/**
	 * Recording sampleRate
	 */
	public static final long DEFAULT_SAMPLERATE = 16000l;
	
	
	/**
	 * 	List of keys in default setting (application default SharedPreferences)
	 * 
	 *  BACKUP_MODE_KEY			: true/false
	 *  AUTO_DOWNLOAD_MODE_KEY  : true/false
	 *  WIFI_MODE_KEY			: true/false
	 *  CELLULAR_MODE_KEY		: true/false
	 *  PUBLIC_SHARE_CONSENT_KEY: true/false
	 *  RESPEAKING_MODE_KEY		: "thumb"
	 *  
	 *  SEARCH_STATE_KEY		: true/false
	 */
	public static final String BACKUP_MODE_KEY = "backup_mode";
	/** */
	public static final String AUTO_DOWNLOAD_MODE_KEY = "autoDownload_mode";
	/** */
	public static final String WIFI_MODE_KEY = "only_wifi";
	/** */
	public static final String CELLULAR_MODE_KEY = "allow_cellular";
	/** */
	public static final String PUBLIC_SHARE_CONSENT_KEY = "public_share";
	/** */
	public static final String RESPEAKING_MODE_KEY = "respeaking_mode";

	/** */
	public static final String SEARCH_STATE_KEY = "search_possible";
	

	/**
	 *  List of keys in each user's setting (SharedPreferences of emailAccount)
	 *  
	 *  APPROVED_RECORDING_KEY	: A set of recording's cloudID approved to be archived
	 *  [upload/archive-approved recording-cloudID]	: <Approved-date>|<archive-state>|<data-store-uri>
	 *  DOWNLOAD_RECORDING_KEY	: A set of recording's cloudID to be downloaded from cloud
	 *  UPLOAD_RECORDING_KEY	: A set of recording's cloudID to be uploaded
	 *  [download-approved recording-cloudID] : <archive-state>
	 *  ARCHIVED_RECORDING_KEY	: A set of recording IDs archived
	 *  
	 *  APPROVED_SPEAKERS_KEY	: A set of speaker's cloudID approved to be archived
	 *  [upload/archive-approved Speaker-cloudID]	: <Approved-date>|<archive-state>|<data-store-uri>
	 *  DOWNLOAD_SPEAKERS_KEY	: A set of speaker's cloudID to be downloaded from cloud
	 *  UPLOAD_SPEAKERS_KEY		: A set of speaker's cloudID to be uploaded
	 *  [download-approved Speaker-cloudID] : <archive-state>
	 *  ARCHIVED_SPEAKERS_KEY	: A set of speaker IDs archived
	 *  
	 *  APPROVED_OTHERS_KEY	: A set of other file's cloudID approved to be archived
	 *  [upload/archive-approved other-file-cloudID]	: <Approved-date>|<archive-state>|<data-store-uri>
	 *  DOWNLOAD_OTHERS_KEY : A set of other file's cloudID to be downloaded from cloud
	 *  UPLOAD_OTHERS_KEY	: A set of other file's cloudID to be uploaded
	 *  [download-approved other-file-cloudID]  : <archive-state>
	 *  ARCHIVED_OTHERS_KEY	: A set of other files' IDs archived (transcript, mapping files)
	 */
	public static final String APPROVED_RECORDING_KEY = "approvedRecordings";
	/** */
	public static final String DOWNLOAD_RECORDING_KEY = "downloadRecordings";
	/** */
	public static final String UPLOAD_RECORDING_KEY = "uploadRecordings";
	/** */
	public static final String ARCHIVED_RECORDING_KEY = "archivedRecordings";
	/** */
	public static final String APPROVED_SPEAKERS_KEY = "approvedSpeakers";
	/** */
	public static final String DOWNLOAD_SPEAKERS_KEY = "downloadSpeakers";
	/** */
	public static final String UPLOAD_SPEAKERS_KEY = "uploadSpeakers";
	/** */
	public static final String ARCHIVED_SPEAKERS_KEY = "archivedSpeakers";
	/** */
	public static final String APPROVED_OTHERS_KEY = "approvedOthers";
	/** */
	public static final String DOWNLOAD_OTHERS_KEY = "downloadOthers";
	/** */
	public static final String UPLOAD_OTHERS_KEY = "uploadOthers";
	/** */
	public static final String ARCHIVED_OTHERS_KEY = "archivedOthers";
	/** */
	public static final String SOURCE_LANG_BUFFER_KEY = "sourceLanguages";
	/** */
	public static final String INTERPRET_LANG_BUFFER_KEY = "interpretLanguages";
	
	
	/**
	 * Return current file-format version
	 * @return	String of version
	 */
	public static String getLatestVersion(){
		return DEFAULT_VERSION;
	}
	
	/**
	 * Return current default owner account
	 * @return	String of default owner account
	 */
	public static String getCurrentUserId() {
		return DEFAULT_USER_ID;
	}
	/**
	 * Set the default owner account
	 * @param Id	String of default owner account
	 */
	public static void setUserId(String Id) {
		DEFAULT_USER_ID = Id;
	}
	
	/**
	 * Return current default owner account's auth-token
	 * @return	String of default owner account's auth-token
	 */
	public static String getCurrentUserToken() {
		return DEFAULT_USER_AUTH_TOKEN;
	}
	/**
	 * Set the default owner account's auth-token
	 * @param token	String of default owner account's auth-token
	 */
	public static void setUserToken(String token) {
		DEFAULT_USER_AUTH_TOKEN = token;
	}
	
	/**
	 * Return current default owner account's ID token.
	 * @return	String of default owner account's ID token.
	 */
	public static String getCurrentUserIdToken() {
		return DEFAULT_USER_ID_TOKEN;
	}

	/**
	 * Set the default owner account's ID token.
	 * @param token	String of default owner account's ID token.
	 */
	public static void setUserIdToken(String token) {
		DEFAULT_USER_ID_TOKEN = token;
	}

	/**
	 * Return current number of users
	 * @return	the number of users
	 */
	public static int getNumberOfUsers() {
		return numOfUsers;
	}
	/**
	 * Set the number of users
	 * @param num	the number of users
	 */
	public static void setNumberOfUsers(int num) {
		numOfUsers = num;
	}
	
	/**
	 * Return current number of speakers
	 * @return	the number of speakers
	 */
	public static int getNumberOfSpeakers() {
		return numOfSpeakers;
	}
	
	/**
	 * Set the number of speakers
	 * @param num	the number of speakers
	 */
	public static void setNumberOfSpeakers(int num) {
		numOfSpeakers = num;
	}
	
	/**
	 * Return current number of items
	 * @return	the number of items
	 */
	public static int getNumberOfItems() {
		return numOfItems;
	}
	/**
	 * Set the number of items
	 * @param num	the number of items
	 */
	public static void setNumberOfItems(int num) {
		numOfItems = num;
	}
	
	/**
	 * Return current ftp upload ratio
	 * @return	the ratio of ftp-upload success
	 */
	public static float getFtpRatio() {
		return ratioOfFtpSync;
	}
	/**
	 * Set the current ftp upload ratio
	 * @param ratio		the ratio of ftp-upload success
	 */
	public static void setFtpRatio(float ratio) {
		ratioOfFtpSync = ratio;
	}
	
	/**
	 * Return current Cloud upload ratio
	 * @return	the ratio of Cloud-upload success
	 */
	public static float getCloudRatio() {
		return ratioOfCloudSync;
	}
	/**
	 * Set the current cloud upload ratio
	 * @param ratio		the ratio of cloud-upload success
	 */
	public static void setCloudRatio(float ratio) {
		ratioOfCloudSync = ratio;
	}
	
	/**
	 * Return current central upload ratio
	 * @return	the ratio of central-upload success
	 */
	public static float getCentralRatio() {
		return ratioOfCentralSync;
	}
	/**
	 * Set the current central upload ratio
	 * @param ratio		the ratio of central-upload success
	 */
	public static void setCentralRatio(float ratio) {
		ratioOfCentralSync = ratio;
	}

	/**
	 * Return the URL of the index server.
	 * @return  the URL of the index server.
	 */
	public static String getIndexServerUrl() {
	    return DEFAULT_INDEX_SERVER;
	}
	
	/**
	 * Return the URL of the file-proxy server
	 * @return	the URL of the file-proxy server
	 */
	public static String getFileProxyServerUrl() {
		return DEFAULT_FILE_PROXY_SERVER;
	}

	/**
	 * Set the URL of the index server.
	 * @param url	URL of central index-server
	 */
	public static void setIndexServerUrl(String url) {
	    DEFAULT_INDEX_SERVER = url;
	}

	/**
     * Return an scope for google-API scope
     * @return the scope of Google-Cloud
     */
    public static String getScope() {
    	String joiner = "";
		String scope = "oauth2:";
		for (String s: GoogleDriveStorage.getScopes()) {
			scope += joiner + s;
			joiner = " ";
		}

		for (String s: FusionIndex.getScopes()) {
			scope += joiner + s;
			joiner = " ";
		}
		return scope;
    }

    /**
     * Return a scope for a client ID of web-application
     * @return	The scope for the web-application(central index-server)
     */
	public static String getIdTokenScope() {
	    return "audience:server:client_id:" + DEFAULT_INDEX_SERVER_CLIENT_ID;
	}
} public final class FileIO {

	/**
	 * Private constructor to prevent instantiation.
	 */
	private FileIO () {}

	/**
	 * The ISO 8601 date format used for parsing dates from JSON files.
	 */
	private static StandardDateFormat standardDateFormat =
			new StandardDateFormat();
	

	/**
	 * The application's top level path in the external storage.
	 */
	static final String APP_ROOT_PATH = "aikuma/";

	/**
	 * The path in the external storage for files that are not to be synced.
	 */
	static final String NO_SYNC_PATH = "aikuma-no-sync/";

	/**
	 * 
	 */
	static final String EXPORT_PATH = "Download";
	
	/**
	 * Returns the path to the application's data.
	 *
	 * @return	A File representing the applications base directory (the "aikuma"
	 * directory)
	 */
	public static File getAppRootPath(){
		File path = new File(Environment.getExternalStorageDirectory(),
				APP_ROOT_PATH);
		path.mkdirs();
		assert path != null;
		return path;
	}
	
	/**
	 * Returns the path to the directory containing files that are not to be
	 * synced.
	 *
	 * @return	A File representing the application's directory for files not
	 * to be synced
	 */
	public static File getNoSyncPath(){
		File path = new File(Environment.getExternalStorageDirectory(),
				NO_SYNC_PATH);
		path.mkdirs();
		return path;
	}
	
	/**
	 * Returns the path to the directory containing files that are exported.
	 *
	 * @return	A File representing the application's directory for files exported
	 */
	public static File getExportPath() {
		File path = new File(Environment.getExternalStorageDirectory(),
				EXPORT_PATH);
		path.mkdirs();
		return path;
	}
	
	/**
	 * Returns the path to the directory containing files of the owner
	 * 
	 * @param versionName	versionName of the files
	 * @param ownerId		Owner's ID for the path
	 * @return	A File representing the owner's directory of versionName
	 */
	public static File getOwnerPath(String versionName, String ownerId) {
		String ownerDirStr = (APP_ROOT_PATH + versionName + "/" + ownerId + "/");	
		File path = new File(Environment.getExternalStorageDirectory(), 
				ownerDirStr);
		path.mkdirs();
		return path;
	}

	/**
	 * Takes a file path (relative to the aikuma directory or absolute) and some
	 * data and writes the data into the file in the aikuma directory in external
	 * storage.
	 *
	 * @param	path	The path to the file in which the data is to be
	 * written.
	 * @param	data	The data that is to be written to the file.
	 * @throws	IOException	If there is an I/O issue when writing the data to
	 * file.
	 */
	public static void write(String path, String data) throws IOException {
		File file;
		// If the path is absolute, use that path, otherwise make it relative
		// to the aikuma directory.
		if (path.startsWith("/")) {
			file = new File(path);
		} else {
			file = new File(getAppRootPath(), path);
		}
		FileUtils.writeStringToFile(file, data, Charsets.UTF_8);
	}

	/**
	 * Takes an absolute file path, some data and
	 * writes the data into the file in the aikuma directory in external storage.
	 *
	 * @param	path	The path to the file in which the data is to be
	 * written.
	 * @param	data	The data that is to be written to the file.
	 * @throws	IOException	If there is an I/O issue when writing the data to file.
	 *
	 */
	public static void write(File path, String data) throws IOException {
		FileUtils.writeStringToFile(path, data, Charsets.UTF_8);
	}

	/**
	 * Takes a File representation of a path and a JSONObject and writes that
	 * JSONObject to the file.
	 *
	 * @param	path	The File where the JSONObject will be written.
	 * @param	jsonObj	The JSONObject to be written.
	 * @throws	IOException if there is an I/O issue when writing the
	 * JSONObject to file.
	 */
	public static void writeJSONObject(File path, JSONObject jsonObj)
			throws IOException {
		StringWriter stringWriter = new StringWriter();
		jsonObj.writeJSONString(stringWriter);
		String jsonStr = stringWriter.toString();
		write(path, jsonStr);
	}
	
	/**
	 * Takes a file path (relative to the aikuma directory) and
	 * returns a string containing the file's contents.
	 *
	 * @param	path	The path to the file in which the data lies.
	 * @throws	IOException	if there is an issue reading from the file.
	 * @return	A string containing the file's contents;
	 */
	public static String read(String path) throws IOException {
		File file;
		// If the path is absolute, use that path, otherwise make it relative
		// to the aikuma directory.
		if (path.startsWith("/")) {
			file = new File(path);
		} else {
			file = new File(getAppRootPath(), path);
		}

		return FileUtils.readFileToString(file, Charsets.UTF_8);
	} 

	/**
	 * Takes a file path (relative to the aikuma directory) and
	 * returns a string containing the file's contents.
	 *
	 * @param	path	The path to the file in which the data lies.
	 * @throws	IOException	If there is an issue reading from the file.
	 * @return	A string containing the file's contents;
	 */
	public static String read(File path) throws IOException {
		return FileUtils.readFileToString(path, Charsets.UTF_8);
	}

	/**
	 * Takes a file path and reads a JSONObject from that file.
	 *
	 * @param	path	The path to the file where the JSON is stored.
	 * @throws	IOException	If there is a problem parsing the JSON.
	 * @return	A JSONObject representing the JSON in the supplied File.
	 */
	public static JSONObject readJSONObject(File path) throws IOException {
		try {
			String jsonStr = read(path);
			JSONParser parser = new JSONParser();
			Object obj = parser.parse(jsonStr);
			JSONObject jsonObj = (JSONObject) obj;
			return jsonObj;
		} catch (org.json.simple.parser.ParseException e) {
			throw new IOException(e);
		}
	}

	/**
	 * Loads the ISO 639-3 language codes from the original text file.
	 *
	 * @param	resources	resources so that the iso 639-3 text file can be
	 * retrieved.
	 * @param languages			[Output] a list of languages having names and corresponding codes
	 * @param languageCodeMap	[Output] a map of a language code to its name
	 * @throws	IOException	If there is an issue reading from the langcodes
	 * file.
	 */
	public static void readLangCodes(Resources resources, 
			List<Language> languages, Map<String, String> languageCodeMap) throws IOException {
		languages.clear();
		languageCodeMap.clear();
		
		InputStream is = resources.openRawResource(R.raw.iso_639_3);
		StringWriter writer = new StringWriter();
		IOUtils.copy(is, writer, Charsets.UTF_8);
		String inputString = writer.toString();
 		String[] lines = inputString.split("\n");
		for (int i = 1; i < lines.length; i++) {
			String[] elements = lines[i].split("(?=\t)");
			languages.add(new Language(elements[6].trim(), elements[0].trim()));
			languageCodeMap.put(elements[0].trim(), elements[6].trim());
		}
		return;
	}

	/**
	 * Writes a specified list of default languages to file.
	 *
	 * @param	defaultLanguages	A list of default languages.
	 * @throws	IOException	If there is a problem with writing the default
	 * languages.
	 */
	public static void writeDefaultLanguages(List<Language> defaultLanguages)
			throws IOException  {
		CSVWriter writer = new CSVWriter(new FileWriter(
				new File(getAppRootPath(), "default_languages.csv")), '\t');
		String[] entry = new String[2];
		for (Language lang : defaultLanguages) {
			entry[0] = lang.getName();
			entry[1] = lang.getCode();
			writer.writeNext(entry);
		}
		writer.close();
	}

	/**
	 * Reads from file the default languages for recording that have been
	 * specified by the user previously.
	 *
	 * @return	An ArrayList of Language objects.
	 */
	public static ArrayList<Language> readDefaultLanguages() {
		ArrayList<Language> defaultLanguages = new ArrayList<Language>();
		try {
			CSVReader reader = new CSVReader(new FileReader(
					new File(getAppRootPath(), "default_languages.csv")), '\t');
			String[] nextLine;
			try {
				while ((nextLine = reader.readNext()) != null) {
					defaultLanguages.add(new Language(nextLine[0], nextLine[1]));
				}
			} catch (IOException e) {
				// Return as many languages as we have managed to retrieve.
				return defaultLanguages;
			}
		} catch (FileNotFoundException e) {
			// Return an empty language list.
			return defaultLanguages;
		}
		return defaultLanguages;
	}

	/**
	 * Deletes the file having path
	 * 
	 * @param path		The path to the file
	 * @throws IOException	if delete fails
	 */
	public static void delete(File path) throws IOException {
		FileUtils.forceDelete(path);
	}
	
	/**
	 * Reads the information about the server into a Server object, using the
	 * default location of server.json in the app's root directory.
	 */
	 /*
	public static Server readServer() throws IOException {
		return FileIO.readServer(
				new File(FileIO.getAppRootPath(), "server.json"));
	}
	*/

	/**
	 * Reads the information about the server into a Server object
	 */
	 /*
	public static Server readServer(File serverInfoFile) throws IOException {
		try {
			JSONParser parser = new JSONParser();
			String jsonStr = FileIO.read(serverInfoFile);
			Object obj = parser.parse(jsonStr);
			JSONObject jsonObj = (JSONObject) obj;
			return new Server(jsonObj.get("ipaddress").toString(),
					jsonObj.get("username").toString(),
					jsonObj.get("password").toString());
		} catch (org.json.simple.parser.ParseException e) {
			throw new IOException(e);
		}
	}
	*/

	/*
	public static void writeServer(Server server) throws IOException {
		FileIO.writeServer(server,
				new File(FileIO.getAppRootPath(), "server.json"));
	}
	*/

	/*
	public static void writeServer(Server server, File serverInfoFile) throws IOException {
		JSONObject obj = new JSONObject();
		obj.put("ipaddress", server.getIPAddress());
		obj.put("username", server.getUsername());
		obj.put("password", server.getPassword());

		StringWriter stringWriter = new StringWriter();
		obj.writeJSONString(stringWriter);
		String jsonText = stringWriter.toString();
		FileIO.write(serverInfoFile, jsonText);
	}
	*/
} public class StandardDateFormat extends SimpleDateFormat {
	/**
	 * Constructor that specifies the date format.
	 */
	public StandardDateFormat() {
		super("yyyy-MM-dd'T'HH:mm:ss'Z'");
		super.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
	}
}
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.FragmentTransaction;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.MediaController;
import android.widget.Toast;
import android.widget.VideoView;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.lp20.aikuma.Aikuma;
import org.lp20.aikuma.model.Language;
import org.lp20.aikuma.model.Recording;
import org.lp20.aikuma.audio.Player;
import org.lp20.aikuma.audio.SimplePlayer;
import org.lp20.aikuma.audio.InterleavedPlayer;
import org.lp20.aikuma.audio.MarkedPlayer;
import org.lp20.aikuma.audio.TranscriptPlayer;
import org.lp20.aikuma2.R;
import org.lp20.aikuma.service.GoogleCloudService;
import org.lp20.aikuma.ui.sensors.ProximityDetector;
import org.lp20.aikuma.util.AikumaSettings;
import org.lp20.aikuma.util.FileIO;
import org.lp20.aikuma.util.StandardDateFormat;
			QuickActionItem privateShareAct =
					new QuickActionItem("share", R.drawable.speakers_32g);
			quickMenu.addActionItem(archiveAct);
			quickMenu.addActionItem(privateShareAct);
		}*/
		QuickActionItem archiveAct = 
				new QuickActionItem("share", R.drawable.aikuma_32);
		QuickActionItem privateShareAct =
				new QuickActionItem("share", R.drawable.speakers_32g);
		quickMenu.addActionItem(archiveAct);
		quickMenu.addActionItem(privateShareAct);
		if(googleAuthToken == null) {
			quickMenu.setItemEnabledAt(3, false);
			quickMenu.setItemEnabledAt(4, false);
		}
		
		quickMenu.addActionItem(respeakAct);
		quickMenu.addActionItem(translateAct);
		quickMenu.addActionItem(refreshAct);
		
		// Tagging buttons
		QuickActionItem tagAct =
				new QuickActionItem("tag", R.drawable.tag_32);
		quickMenu.addActionItem(tagAct);
		
		// Segmenting button
		QuickActionItem segAct =
				new QuickActionItem("seg", R.drawable.seg_32);
		quickMenu.addActionItem(segAct);
		
		// Export button
		QuickActionItem exportAct = 
				new QuickActionItem("export", R.drawable.export_32);
		quickMenu.addActionItem(exportAct);
		
		//setup the action item click listener
		quickMenu.setOnActionItemClickListener(new QuickActionMenu.OnActionItemClickListener<Recording>() {			
			@Override
			public void onItemClick(int pos, Recording recording) {
				
				if (pos == 0) { //Add item selected
					onStarButtonPressed(null);
				} else if (pos == 1) { //Accept item selected
					onFlagButtonPressed(null);
				} else if (pos == 2) { //Upload item selected
					onShareButtonPressed(null);
				} else if (pos == 3) {
					onArchiveButtonPressed(null);
				} else if (pos == 4) {
					onPrivateShareButtonPressed(null);
				} else if (pos == 5) {
					//respeak
					onRespeakButton(null, "respeak");
				} else if (pos == 6) {
					//translate
					onInterpretButtonPressed(null);
				} else if (pos == 7) {
					//refresh
				} else if (pos == 8) {
					// tag
					onTagButtonPressed(null);
				} else if (pos == 9) {
					onSegButtonPressed(null, "segment");
				} else if (pos == 10) {
					onExportButtonPressed(null);
				}
			}
		});
	}

	// Set up the video-player
	private void setUpVideoView() {
		videoView.setVideoPath(recording.getFile().getAbsolutePath());
		videoView.setMediaController(new MediaController(this));
	}
	
	// Set up the player
	private void setUpPlayer() {
		try {
			if (recording.isOriginal()) {
				// If there is a transcript for the recording
				// TranscriptPlayer is loaded
				if(recording.getTranscript() != null) {
					TranscriptPlayer player =
							new TranscriptPlayer(recording, this);
					
					// TODO: How to deal with transcripts needs to be decided
					findViewById(R.id.topLine).setVisibility(View.VISIBLE);
					findViewById(R.id.transcriptView).setVisibility(View.VISIBLE);
					findViewById(R.id.middleLine).setVisibility(View.VISIBLE);
					findViewById(R.id.transcriptView2).setVisibility(View.VISIBLE);
					findViewById(R.id.bottomLine).setVisibility(View.VISIBLE);

					setPlayer(player);
				} else {
					setPlayer(new SimplePlayer(recording, true));
				}
				
			} else {
				setPlayer(new InterleavedPlayer(this, recording));
				/*
				ImageButton respeakingButton =
						(ImageButton) findViewById(R.id.respeaking);
				respeakingButton.setVisibility(View.GONE);
				*/
			}
		} catch (IOException e) {
			//The player couldn't be created from the recoridng, so lets wrap
			//this activity up.
			Toast.makeText(this, "Failed to create player from recording.",
					Toast.LENGTH_LONG).show();
			ListenActivity.this.finish();
		}
	}

	private void setPlayer(SimplePlayer player) {
		this.player = player;
		fragment.setPlayer(player);
	}

	private void setPlayer(MarkedPlayer player) {