Java tutorial
/* * Ubuntu One Files - access Ubuntu One cloud storage on Android platform. * * Copyright 2011-2012 Canonical Ltd. * * This file is part of Ubuntu One Files. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see http://www.gnu.org/licenses */ package com.ubuntuone.android.files.service; import java.io.File; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Intent; import android.content.OperationApplicationException; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.support.v4.content.LocalBroadcastManager; import com.ubuntuone.android.files.Constants; import com.ubuntuone.android.files.Preferences; import com.ubuntuone.android.files.UbuntuOneFiles; import com.ubuntuone.android.files.provider.MetaContract; import com.ubuntuone.android.files.provider.MetaContract.Nodes; import com.ubuntuone.android.files.provider.MetaContract.ResourceState; import com.ubuntuone.android.files.provider.MetaContract.Volumes; import com.ubuntuone.android.files.provider.MetaUtilities; import com.ubuntuone.android.files.util.Authorizer; import com.ubuntuone.android.files.util.AwakeIntentService; import com.ubuntuone.android.files.util.FileUtilities; import com.ubuntuone.android.files.util.HttpManager; import com.ubuntuone.android.files.util.Log; import com.ubuntuone.android.files.util.NetworkUtil; import com.ubuntuone.api.files.U1FileAPI; import com.ubuntuone.api.files.model.U1File; import com.ubuntuone.api.files.model.U1Node; import com.ubuntuone.api.files.model.U1NodeKind; import com.ubuntuone.api.files.model.U1User; import com.ubuntuone.api.files.model.U1Volume; import com.ubuntuone.api.files.request.U1NodeListener; import com.ubuntuone.api.files.request.U1UserListener; import com.ubuntuone.api.files.request.U1VolumeListener; import com.ubuntuone.api.files.util.U1Failure; import com.ubuntuone.api.sso.authorizer.OAuthAuthorizer; import com.ubuntuone.api.sso.exceptions.TimeDriftException; public class MetaService extends AwakeIntentService { private static final String TAG = MetaService.class.getSimpleName(); private static final String BASE = "com.ubuntuone.android.files"; public static final String ACTION_GET_USER = BASE + ".ACTION_GET_USER"; public static final String ACTION_GET_VOLUME = BASE + ".ACTION_GET_VOLUME"; public static final String ACTION_CREATE_VOLUME = BASE + ".ACTION_CREATE_VOLUME"; public static final String ACTION_MAKE_DIRECTORY = BASE + ".ACTION_MAKE_DIRECTORY"; public static final String ACTION_GET_NODE = BASE + ".ACTION_GET_NODE"; public static final String ACTION_UPDATE_NODE = BASE + ".ACTION_UPDATE_NODE"; public static final String ACTION_DELETE_NODE = BASE + ".ACTION_DELETE_NODE"; /** Auto-upload media request. */ public static final String ACTION_UPLOAD_MEDIA = BASE + ".ACTION_UPLOAD_MEDIA"; public static final String EXTRA_CALLBACK = "extra_callback"; public static final String EXTRA_TIMESTAMP = "extra_timestamp"; public static final String EXTRA_ERROR = "extra_error"; public static final String EXTRA_RESOURCE_PATH = Nodes.NODE_RESOURCE_PATH; public static final String EXTRA_ID = Nodes._ID; public static final String EXTRA_PATH = Nodes.NODE_PATH; public static final String EXTRA_KIND = Nodes.NODE_KIND; public static final String EXTRA_NAME = Nodes.NODE_NAME; public static final String EXTRA_SIZE = Nodes.NODE_SIZE; public static final String EXTRA_HAS_CHILDREN = Nodes.NODE_HAS_CHILDREN; public static interface Status { public final int PENDING = 1; public final int RUNNING = 2; public final int PROGRESS = 3; public final int FINISHED = 4; public final int ERROR = 5; } public static boolean sSyncRunning = false; private ContentResolver contentResolver; private HttpClient httpClient; private OAuthAuthorizer authorizer; private U1FileAPI api; public MetaService() { super(MetaService.class.getSimpleName()); } @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate()"); contentResolver = getContentResolver(); httpClient = HttpManager.getClient(); try { authorizer = Authorizer.getInstance(httpClient, false); } catch (TimeDriftException e) { Log.e(TAG, "TimeDriftException in MetaService"); } catch (Exception e) { } api = new U1FileAPI(UbuntuOneFiles.class.getPackage().getName(), Preferences.getSavedVersionName(), Constants.U1_METADATA_HOST, Constants.U1_CONTENT_HOST, httpClient, authorizer); } @Override protected void onHandleIntent(Intent intent) { sSyncRunning = true; Thread.currentThread().setPriority(Thread.MIN_PRIORITY); final String action = intent.getAction(); final String resourcePath = intent.getStringExtra(EXTRA_RESOURCE_PATH); final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_CALLBACK); if (ACTION_GET_USER.equals(action)) { getUser(receiver); getVolumes(receiver); } else if (ACTION_GET_VOLUME.equals(action)) { getVolume(resourcePath, receiver); } else if (ACTION_CREATE_VOLUME.equals(action)) { createVolume(resourcePath, receiver); } else if (ACTION_MAKE_DIRECTORY.equals(action)) { makeDirectory(resourcePath, receiver); } else if (ACTION_GET_NODE.equals(action)) { getNode(resourcePath, receiver, true); } else if (ACTION_UPDATE_NODE.equals(action)) { if (intent.hasExtra(Nodes.NODE_NAME)) { String newPath = intent.getStringExtra(EXTRA_PATH); updateNode(resourcePath, newPath, receiver); } if (intent.hasExtra(Nodes.NODE_IS_PUBLIC)) { Boolean isPublic = intent.getBooleanExtra(Nodes.NODE_IS_PUBLIC, false); updateNode(resourcePath, isPublic, receiver); } } else if (ACTION_DELETE_NODE.equals(action)) { deleteNode(resourcePath, receiver); } sSyncRunning = false; } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy()"); } public static boolean isSyncRunning() { return sSyncRunning; } public void onUbuntuOneFailure(U1Failure failure, ResultReceiver receiver) { Log.d(TAG, "onUbuntuOneFailure: " + failure.toString() + ", status code: " + failure.getStatusCode()); final Bundle resultData = new Bundle(); resultData.putString(EXTRA_ERROR, failure.getMessage()); if (receiver != null) { receiver.send(Status.ERROR, resultData); } } public void onFailure(U1Failure failure, ResultReceiver receiver) { int statusCode = failure.getStatusCode(); Log.e(TAG, "onFailure: " + failure.toString() + ", HTTP " + statusCode); Bundle data; switch (statusCode) { case HttpStatus.SC_UNAUTHORIZED: Preferences.invalidateToken(this); Intent intent = new Intent("com.ubuntuone.android.files.SIGN_OUT"); LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); stopSelf(); break; case HttpStatus.SC_NOT_FOUND: data = new Bundle(); data.putString(EXTRA_ERROR, "Resource not found."); receiver.send(Status.ERROR, data); break; case HttpStatus.SC_INTERNAL_SERVER_ERROR: data = new Bundle(); data.putString(EXTRA_ERROR, "Server error. Please try again later."); receiver.send(Status.ERROR, data); break; default: // TODO GA? break; } } private void getUser(final ResultReceiver receiver) { api.getUser(new U1UserListener() { @Override public void onStart() { if (receiver != null) receiver.send(Status.RUNNING, Bundle.EMPTY); } @Override public void onSuccess(U1User user) { onGetUserSuccess(user); } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { if (receiver != null) receiver.send(Status.FINISHED, Bundle.EMPTY); } }); } public void onGetUserSuccess(U1User user) { Preferences.updateAccountInfo(user); } public void getVolumes(final ResultReceiver receiver) { // Cached volume node paths. final Set<String> cachedNodePaths = MetaUtilities.getUserNodePaths(); // Fresh volume node paths. final List<String> volumeNodePaths = new LinkedList<String>(); api.getVolumes(new U1VolumeListener() { @Override public void onStart() { if (receiver != null) receiver.send(Status.RUNNING, Bundle.EMPTY); } @Override public void onSuccess(U1Volume volume) { onGetVolumeSuccess(volume, receiver); volumeNodePaths.add(volume.getNodePath()); } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { for (String nodePath : volumeNodePaths) { getNode(nodePath, null, false); cachedNodePaths.remove(nodePath); } if (NetworkUtil.isConnected(MetaService.this)) { // We are connected, thus left cachedNodePaths are invalid. for (String oldNodePath : cachedNodePaths) { MetaUtilities.cleanupTreeByResourcePath(oldNodePath); } } contentResolver.notifyChange(Nodes.CONTENT_URI, null); if (receiver != null) receiver.send(Status.FINISHED, Bundle.EMPTY); } }); } public void getVolume(final String resourcePath, final ResultReceiver receiver) { final Bundle data = new Bundle(); data.putString(EXTRA_RESOURCE_PATH, resourcePath); api.getVolume(resourcePath, new U1VolumeListener() { @Override public void onStart() { if (receiver != null) receiver.send(Status.RUNNING, data); } @Override public void onSuccess(U1Volume volume) { onGetVolumeSuccess(volume, receiver); } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { if (receiver != null) receiver.send(Status.FINISHED, data); } }); } private void onGetVolumeSuccess(U1Volume volume, ResultReceiver receiver) { final String resourcePath = volume.getResourcePath(); ContentValues values = Volumes.valuesFromRepr(volume); String selection = Volumes.VOLUME_RESOURCE_PATH + "=?"; String[] selectionArgs = new String[] { resourcePath }; long savedGeneration = MetaUtilities.getVolumeGeneration(resourcePath); int updated = contentResolver.update(Volumes.CONTENT_URI, values, selection, selectionArgs); if (updated == 0) { contentResolver.insert(Volumes.CONTENT_URI, values); } else { if (savedGeneration > 0 && savedGeneration < volume.getGeneration()) { getVolumeDelta(volume.getResourcePath(), savedGeneration, receiver); } } } public void getVolumeDelta(String resourcePath, long generation, final ResultReceiver receiver) { final Bundle data = new Bundle(); data.putString(EXTRA_RESOURCE_PATH, resourcePath); api.getVolumeDelta(resourcePath, generation, new U1NodeListener() { @Override public void onStart() { if (receiver != null) receiver.send(Status.RUNNING, data); } @Override public void onSuccess(U1Node node) { onGetNodeSuccess(node, false); } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { if (receiver != null) receiver.send(Status.FINISHED, data); } }); } public void getNode(final String resourcePath, final ResultReceiver receiver, final boolean getChildren) { final Bundle data = new Bundle(); data.putString(EXTRA_RESOURCE_PATH, resourcePath); api.getNode(resourcePath, new U1NodeListener() { @Override public void onStart() { if (receiver != null) receiver.send(Status.RUNNING, data); } @Override public void onSuccess(U1Node node) { onGetNodeSuccess(node, getChildren); } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { if (receiver != null) receiver.send(Status.FINISHED, data); } }); } public void onGetNodeSuccess(U1Node node, boolean getChildren) { final String resourcePath = node.getResourcePath(); final boolean isDirectory = node.getKind() == U1NodeKind.DIRECTORY; String data = null; String oldHash = MetaUtilities.getStringField(resourcePath, Nodes.NODE_HASH); if (node.getKind() == U1NodeKind.FILE && oldHash != null) { String newHash = ((U1File) node).getHash(); if (!oldHash.equals(newHash)) { String path = FileUtilities.getFilePathFromResourcePath(node.getResourcePath()); FileUtilities.removeSilently(path); data = ""; } } MetaUtilities.updateNode(getContentResolver(), node, data); if (node.getIsLive()) { if (isDirectory && getChildren) { getDirectoryNode(resourcePath, null); } } } private void createVolume(final String resourcePath, final ResultReceiver receiver) { api.createVolume(resourcePath, new U1VolumeListener() { @Override public void onStart() { if (receiver != null) receiver.send(Status.RUNNING, Bundle.EMPTY); } @Override public void onSuccess(U1Volume volume) { onCreateVolumeSuccess(volume); } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { if (receiver != null) { Bundle data = new Bundle(); data.putString(EXTRA_RESOURCE_PATH, resourcePath); receiver.send(Status.FINISHED, data); } } }); } /** * Volumes need to be inserted manually, since their name on the list is * not simply the last segment of the path, but their full path. Volume * names are not updated when volume root nodes are refreshed. * * @param volume * The volume, which has been created. */ private void onCreateVolumeSuccess(U1Volume volume) { final ContentValues values = new ContentValues(); values.put(Nodes.NODE_RESOURCE_PATH, volume.getNodePath()); values.put(Nodes.NODE_KIND, U1NodeKind.DIRECTORY.toString()); values.put(Nodes.NODE_PATH, volume.getPath()); values.put(Nodes.NODE_NAME, volume.getPath()); getContentResolver().insert(Nodes.CONTENT_URI, values); getNode(volume.getNodePath(), null, false); } private void makeDirectory(final String resourcePath, final ResultReceiver receiver) { final Bundle data = new Bundle(); data.putString(EXTRA_RESOURCE_PATH, resourcePath); api.makeDirectory(resourcePath, new U1NodeListener() { @Override public void onStart() { if (receiver != null) receiver.send(Status.RUNNING, Bundle.EMPTY); } @Override public void onSuccess(U1Node node) { onMakeDirectorySuccess(node); } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { if (receiver != null) receiver.send(Status.FINISHED, data); } }); } public void onMakeDirectorySuccess(U1Node node) { onGetNodeSuccess(node, false); } private void updateNode(final String resourcePath, Boolean isPublic, final ResultReceiver receiver) { final Bundle data = new Bundle(); data.putString(EXTRA_RESOURCE_PATH, resourcePath); api.setFilePublic(resourcePath, isPublic, new U1NodeListener() { @Override public void onStart() { if (receiver != null) receiver.send(Status.RUNNING, Bundle.EMPTY); } @Override public void onSuccess(U1Node node) { onUpdateNodeAccessSuccess(node); } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { if (receiver != null) receiver.send(Status.FINISHED, data); } }); } private void onUpdateNodeAccessSuccess(U1Node node) { onGetNodeSuccess(node, false); } private void updateNode(final String resourcePath, final String newPath, final ResultReceiver receiver) { final Bundle data = new Bundle(); data.putString(EXTRA_RESOURCE_PATH, resourcePath); api.moveNode(resourcePath, newPath, new U1NodeListener() { @Override public void onStart() { if (receiver != null) receiver.send(Status.RUNNING, data); } @Override public void onSuccess(U1Node node) { onUpdateNodeNameSuccess(resourcePath, node); } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { if (receiver != null) receiver.send(Status.FINISHED, data); } }); } private void onUpdateNodeNameSuccess(String resourcePath, U1Node node) { final String newResourcePath = node.getResourcePath(); // Rename the file, if cached. String path = FileUtilities.getFilePathFromResourcePath(resourcePath); // This changes filename in place, mv not supported yet. final File oldFile = new File(path); if (oldFile.exists()) { String newPath = FileUtilities.getFilePathFromResourcePath(newResourcePath); final File newFile = new File(newPath); Log.d(TAG, "Renaming cached file " + oldFile + " to " + newFile); oldFile.renameTo(newFile); // Update the cache. if (newFile.isFile()) { final String newFileData = newFile.getAbsolutePath(); MetaUtilities.updateStringField(resourcePath, Nodes.NODE_DATA, newFileData); } } MetaUtilities.setIsCached(resourcePath, false); onGetNodeSuccess(node, false); } private void deleteNode(final String resourcePath, final ResultReceiver receiver) { final Bundle data = new Bundle(); data.putString(EXTRA_RESOURCE_PATH, resourcePath); api.deleteNode(resourcePath, new U1NodeListener() { @Override public void onStart() { receiver.send(Status.RUNNING, data); MetaUtilities.setState(resourcePath, ResourceState.STATE_DELETING); MetaUtilities.notifyChange(Nodes.CONTENT_URI); } @Override public void onSuccess(U1Node node) { MetaUtilities.deleteByResourcePath(resourcePath); } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { MetaUtilities.deleteByResourcePath(resourcePath); MetaUtilities.notifyChange(Nodes.CONTENT_URI); if (receiver != null) receiver.send(Status.FINISHED, data); } }); } /** * Given parents resource path and {@link ArrayList} of {@link NodeInfo}s of * its children, syncs cached info of these children. Updating children in * one method enables us to make use of database transaction.<br /> * <ul> * <li>- inserts if child is new</li> * <li>- updates if child has changed [thus marks is_cached = false]</li> * <li>- deletes if child is missing [dead node]</li> * </ul> * * @param parentResourcePath * the resource path of childrens parent * @param children * {@link NodeInfo}s of the parents children * @throws OperationApplicationException * @throws RemoteException */ public void getDirectoryNode(final String resourcePath, final ResultReceiver receiver) { Log.i(TAG, "getDirectoryNode()"); final String[] projection = new String[] { Nodes._ID, Nodes.NODE_RESOURCE_PATH, Nodes.NODE_GENERATION, Nodes.NODE_DATA }; final String selection = Nodes.NODE_RESOURCE_PATH + "=?"; final Set<Integer> childrenIds = MetaUtilities.getChildrenIds(resourcePath); final ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); final Bundle data = new Bundle(); data.putString(EXTRA_RESOURCE_PATH, resourcePath); api.listDirectory(resourcePath, new U1NodeListener() { @Override public void onStart() { if (receiver != null) receiver.send(Status.RUNNING, data); } @Override public void onSuccess(U1Node node) { if (node.getKind() == U1NodeKind.FILE && ((U1File) node).getSize() == null) { // Ignore files with null size. return; } final String[] selectionArgs = new String[] { node.getResourcePath() }; final Cursor c = contentResolver.query(Nodes.CONTENT_URI, projection, selection, selectionArgs, null); try { ContentValues values = Nodes.valuesFromRepr(node); if (c.moveToFirst()) { final int id = c.getInt(c.getColumnIndex(Nodes._ID)); // Node is live. childrenIds.remove(id); // Update node. final long generation = c.getLong(c.getColumnIndex(Nodes.NODE_GENERATION)); final long newGeneration = node.getGeneration(); if (generation < newGeneration) { Log.v(TAG, "updating child node, new generation"); values.put(Nodes.NODE_IS_CACHED, false); values.put(Nodes.NODE_DATA, ""); String data = c.getString(c.getColumnIndex(Nodes.NODE_DATA)); FileUtilities.removeSilently(data); Uri uri = MetaUtilities.buildNodeUri(id); ContentProviderOperation op = ContentProviderOperation.newUpdate(uri).withValues(values) .build(); operations.add(op); if (operations.size() > 10) { try { contentResolver.applyBatch(MetaContract.CONTENT_AUTHORITY, operations); operations.clear(); } catch (RemoteException e) { Log.e(TAG, "Remote exception", e); } catch (OperationApplicationException e) { MetaUtilities.setIsCached(resourcePath, false); return; } Thread.yield(); } } else { Log.v(TAG, "child up to date"); } } else { // Insert node. Log.v(TAG, "inserting child"); ContentProviderOperation op = ContentProviderOperation.newInsert(Nodes.CONTENT_URI) .withValues(values).build(); operations.add(op); if (operations.size() > 10) { try { contentResolver.applyBatch(MetaContract.CONTENT_AUTHORITY, operations); operations.clear(); } catch (RemoteException e) { Log.e(TAG, "Remote exception", e); } catch (OperationApplicationException e) { MetaUtilities.setIsCached(resourcePath, false); return; } Thread.yield(); } } } finally { c.close(); } } @Override public void onUbuntuOneFailure(U1Failure failure) { MetaService.this.onUbuntuOneFailure(failure, receiver); } @Override public void onFailure(U1Failure failure) { MetaService.this.onFailure(failure, receiver); } @Override public void onFinish() { if (receiver != null) receiver.send(Status.FINISHED, data); } }); // Remove nodes, which ids are left in childrenIds set. if (!childrenIds.isEmpty()) { Log.v(TAG, "childrenIDs not empty: " + childrenIds.size()); final Iterator<Integer> it = childrenIds.iterator(); while (it.hasNext()) { int id = it.next(); Uri uri = MetaUtilities.buildNodeUri(id); ContentProviderOperation op = ContentProviderOperation.newDelete(uri).build(); operations.add(op); } } else { Log.v(TAG, "childrenIDs empty"); } try { long then = System.currentTimeMillis(); contentResolver.applyBatch(MetaContract.CONTENT_AUTHORITY, operations); MetaUtilities.setIsCached(resourcePath, true); long now = System.currentTimeMillis(); Log.d(TAG, "time to update children: " + (now - then)); contentResolver.notifyChange(Nodes.CONTENT_URI, null); } catch (RemoteException e) { Log.e(TAG, "", e); } catch (OperationApplicationException e) { MetaUtilities.setIsCached(resourcePath, false); return; } } }