/* * Copyright (C) 2012 Simon Robinson * * This file is part of Com-Me. * * Com-Me is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * Com-Me is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General * Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with Com-Me. * If not, see <http://www.gnu.org/licenses/>. */ package ac.robinson.mediatablet.importing; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.LineNumberReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Map; import ac.robinson.mediatablet.MediaTablet; import ac.robinson.mediautilities.MediaUtilities; import ac.robinson.mediautilities.SMILUtilities; import ac.robinson.util.DebugUtilities; import ac.robinson.util.IOUtilities; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.FileObserver; import android.os.Handler; import android.os.Message; import android.util.Log; public class BluetoothObserver extends FileObserver { // synchronized because onEvent() runs on a separate thread private Map<String, Map<String, Boolean>> mSMILContents = Collections .synchronizedMap(new HashMap<String, Map<String, Boolean>>()); private String mPreviousExport = null; // for tracking duplicates private final Handler mHandler; private final String mBluetoothDirectoryPath; /** * Create a new BluetoothObserver for a directory * * @param path The directory to monitor * @param mask Currently ignored; will only monitor FileObserver.CLOSE_WRITE * @param handler The handler used to send messages when media files are received */ public BluetoothObserver(String path, int mask, Handler handler) { // path *MUST* end with '/' super((path.endsWith(File.separator) ? path : path + File.separator), FileObserver.CLOSE_WRITE); mBluetoothDirectoryPath = (path.endsWith(File.separator) ? path : path + File.separator); mHandler = handler; } /** * Equivalent to BluetoothObserver(path, FileObserver.ALL_EVENTS, handler); */ public BluetoothObserver(String path, Handler handler) { this(path, FileObserver.ALL_EVENTS, handler); } public static void setBluetoothVisibility(Context context, boolean visible) { // TODO: preserve previous state? BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); int currentBluetoothMode = bluetoothAdapter.getScanMode(); // TODO: save initially and restore? if (visible && currentBluetoothMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverableIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0); // forever context.startActivity(discoverableIntent); } else if (!visible && currentBluetoothMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { // TODO: turn off } } private void sendMessage(int messageId, String storyFilePath) { Message msg = mHandler.obtainMessage(messageId); Bundle bundle = new Bundle(); bundle.putString(MediaUtilities.KEY_FILE_NAME, storyFilePath); msg.setData(bundle); mHandler.sendMessage(msg); } private String fileIsRequiredForSMIL(String filePath) { for (String smilFile : mSMILContents.keySet()) { if (mSMILContents.get(smilFile).containsKey(filePath)) { return smilFile; } } return null; } private boolean checkAndSendSMILContents(String smilParent, Map<String, Boolean> smilContents) { // check that the files themselves exist (i.e. haven't been deleted since we first received them) for (String mediaFile : smilContents.keySet()) { if (new File(mediaFile).exists()) { smilContents.put(mediaFile, true); } else { smilContents.put(mediaFile, false); } } // check that we have all the required files boolean allContentsComplete = true; for (String mediaFile : smilContents.keySet()) { if (!smilContents.get(mediaFile)) { allContentsComplete = false; } } // send the message and reset if (allContentsComplete) { sendMessage(MediaUtilities.MSG_RECEIVED_SMIL_FILE, smilParent); mPreviousExport = smilParent; mSMILContents.remove(smilParent); if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "Sending SMIL"); return true; } if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "SMIL not yet complete - waiting"); return false; } @Override public void onEvent(int event, String path) { if (path == null) { return; } // see: http://developer.android.com/reference/android/os/FileObserver.html switch (event) { case CLOSE_WRITE: File receivedFile = new File(mBluetoothDirectoryPath, path); if (receivedFile.length() <= 0) { // on some platforms the file is created before permission is granted break; } // handle key files - html and smil String fileAbsolutePath = receivedFile.getAbsolutePath(); if (IOUtilities.fileExtensionIs(fileAbsolutePath, MediaUtilities.HTML_FILE_EXTENSION)) { // html is a simple single-file import, but an html file is also sent for smil - need to ignore it if (fileIsRequiredForSMIL(fileAbsolutePath) == null) { FileReader fileReader = null; LineNumberReader lineNumberReader = null; try { fileReader = new FileReader(receivedFile); lineNumberReader = new LineNumberReader(fileReader); // only send if it's an html5 player file String firstLine = lineNumberReader.readLine(); if ("<!DOCTYPE html>".equals(firstLine)) { // hack! sendMessage(MediaUtilities.MSG_RECEIVED_HTML_FILE, fileAbsolutePath); if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "Sending HTML"); break; } } catch (FileNotFoundException e) { } catch (IOException e) { } finally { IOUtilities.closeStream(lineNumberReader); IOUtilities.closeStream(fileReader); } } } else if (IOUtilities.fileExtensionIs(fileAbsolutePath, MediaUtilities.SMIL_FILE_EXTENSION) || IOUtilities.fileExtensionIs(fileAbsolutePath, MediaUtilities.SYNC_FILE_EXTENSION)) { // need to deal with some devices automatically deleting anything with a .smil extension - .sync.jpg // is the same as the .smil contents, but with a .jpg file extension if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "Starting to parse SMIL: " + receivedFile.getName()); // don't add the same key twice - could confuse things a lot if (!mSMILContents.containsKey(fileAbsolutePath)) { // we've parsed the .smil and now have the .sync.jpg, or vice-versa - need to deal with this String previousFile = null; if (fileAbsolutePath.endsWith(MediaUtilities.SYNC_FILE_EXTENSION)) { previousFile = fileAbsolutePath.replace(MediaUtilities.SYNC_FILE_EXTENSION, MediaUtilities.SMIL_FILE_EXTENSION); } else if (fileAbsolutePath.endsWith(MediaUtilities.SMIL_FILE_EXTENSION)) { previousFile = fileAbsolutePath.replace(MediaUtilities.SMIL_FILE_EXTENSION, MediaUtilities.SYNC_FILE_EXTENSION); } if (previousFile != null // the file could exist if we're still processing it from this import && (mSMILContents.containsKey(previousFile) || previousFile.equals(mPreviousExport))) { mPreviousExport = null; receivedFile.delete(); // because otherwise we'll miss it, regardless of deletion prefs if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "Found duplicate SMIL/sync file - deleting: " + receivedFile.getName()); break; } Map<String, Boolean> smilContents = Collections.synchronizedMap(new HashMap<String, Boolean>()); // TODO: we include non-media elements so we can delete them; // but importing successfully is more important than deleting all files... ArrayList<String> smilUnparsedContents = SMILUtilities .getSimpleSMILFileList(receivedFile, true); if (smilUnparsedContents != null) { for (String mediaFile : smilUnparsedContents) { File smilMediaFile = new File(receivedFile.getParent(), mediaFile); String smilMediaPath = smilMediaFile.getAbsolutePath(); // in case the file has already been received if (smilMediaFile.exists()) { smilContents.put(smilMediaPath, true); if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "SMIL component found (file exists): " + smilMediaPath); } else { if (!smilMediaPath.endsWith(MediaUtilities.SYNC_FILE_EXTENSION)) { smilContents.put(smilMediaPath, false); if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "SMIL component not yet sent: " + smilMediaPath); } else { Log.d(DebugUtilities.getLogTag(this), "SMIL sync component found (ignoring): " + smilMediaPath); } } } mSMILContents.put(fileAbsolutePath, smilContents); } else { // error - couldn't parse the smil file if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "SMIL parse error: " + receivedFile.getName()); } checkAndSendSMILContents(fileAbsolutePath, smilContents); break; } else { // error - tried to import the same file twice; ignored if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "SMIL already parsed - ignoring: " + receivedFile.getName()); } } // handle any other files String smilParent = fileIsRequiredForSMIL(fileAbsolutePath); if (smilParent != null) { Map<String, Boolean> smilContents = mSMILContents.get(smilParent); if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "SMIL component received: " + fileAbsolutePath); // update the list - we now have this file smilContents.remove(fileAbsolutePath); smilContents.put(fileAbsolutePath, true); checkAndSendSMILContents(smilParent, smilContents); } else { if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "Importing non-SMIL component: " + fileAbsolutePath); // note that importing files deletes them from the bluetooth directory (to preserve space), so to // import SMIL narratives the SMIL or sync file must be sent before any media items sendMessage(MediaUtilities.MSG_RECEIVED_IMPORT_FILE, fileAbsolutePath); } break; case ACCESS: case ATTRIB: case CLOSE_NOWRITE: case CREATE: case DELETE: case DELETE_SELF: case MODIFY: case MOVED_FROM: case MOVED_TO: case MOVE_SELF: case OPEN: default: if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "Other event: " + event); break; } } @Override public void startWatching() { super.startWatching(); if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "Initialising/refreshing - watching " + mBluetoothDirectoryPath); } @Override public void stopWatching() { super.stopWatching(); mSMILContents.clear(); if (MediaTablet.DEBUG) Log.d(DebugUtilities.getLogTag(this), "Stopping - no longer watching " + mBluetoothDirectoryPath); } }