diff options
70 files changed, 2143 insertions, 362 deletions
diff --git a/api/current.txt b/api/current.txt index 62f0113a7d1c..3e63cd7237e1 100644 --- a/api/current.txt +++ b/api/current.txt @@ -26218,6 +26218,7 @@ package android.provider { field public static final int FLAG_SUPPORTS_MOVE = 256; // 0x100 field public static final int FLAG_SUPPORTS_RENAME = 64; // 0x40 field public static final int FLAG_SUPPORTS_THUMBNAIL = 1; // 0x1 + field public static final int FLAG_SUPPORTS_TYPED_DOCUMENT = 512; // 0x200 field public static final int FLAG_SUPPORTS_WRITE = 2; // 0x2 field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.document/directory"; } @@ -26255,6 +26256,7 @@ package android.provider { method public final android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException; method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle) throws java.io.FileNotFoundException; method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException; + method public android.content.res.AssetFileDescriptor openTypedDocument(java.lang.String, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException; method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String); method public abstract android.database.Cursor queryChildDocuments(java.lang.String, java.lang.String[], java.lang.String) throws java.io.FileNotFoundException; method public abstract android.database.Cursor queryDocument(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; @@ -26781,6 +26783,7 @@ package android.provider { field public static final deprecated java.lang.String DEVICE_PROVISIONED = "device_provisioned"; field public static final java.lang.String ENABLED_ACCESSIBILITY_SERVICES = "enabled_accessibility_services"; field public static final java.lang.String ENABLED_INPUT_METHODS = "enabled_input_methods"; + field public static final java.lang.String ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES = "enabled_notification_policy_access_packages"; field public static final deprecated java.lang.String HTTP_PROXY = "http_proxy"; field public static final java.lang.String INPUT_METHOD_SELECTOR_VISIBILITY = "input_method_selector_visibility"; field public static final java.lang.String INSTALL_NON_MARKET_APPS = "install_non_market_apps"; diff --git a/api/system-current.txt b/api/system-current.txt index 4118f338ed64..bd674ef65f0b 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -3878,6 +3878,7 @@ package android.app { method public void set(int, long, android.app.PendingIntent); method public void set(int, long, java.lang.String, android.app.AlarmManager.OnAlarmListener, android.os.Handler); method public void set(int, long, long, long, android.app.PendingIntent, android.os.WorkSource); + method public void set(int, long, long, long, android.app.AlarmManager.OnAlarmListener, android.os.Handler, android.os.WorkSource); method public void setAlarmClock(android.app.AlarmManager.AlarmClockInfo, android.app.PendingIntent); method public void setAndAllowWhileIdle(int, long, android.app.PendingIntent); method public void setExact(int, long, android.app.PendingIntent); @@ -28207,6 +28208,7 @@ package android.provider { field public static final int FLAG_SUPPORTS_MOVE = 256; // 0x100 field public static final int FLAG_SUPPORTS_RENAME = 64; // 0x40 field public static final int FLAG_SUPPORTS_THUMBNAIL = 1; // 0x1 + field public static final int FLAG_SUPPORTS_TYPED_DOCUMENT = 512; // 0x200 field public static final int FLAG_SUPPORTS_WRITE = 2; // 0x2 field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.document/directory"; } @@ -28244,6 +28246,7 @@ package android.provider { method public final android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException; method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle) throws java.io.FileNotFoundException; method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException; + method public android.content.res.AssetFileDescriptor openTypedDocument(java.lang.String, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException; method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String); method public abstract android.database.Cursor queryChildDocuments(java.lang.String, java.lang.String[], java.lang.String) throws java.io.FileNotFoundException; method public abstract android.database.Cursor queryDocument(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; @@ -28873,6 +28876,7 @@ package android.provider { field public static final deprecated java.lang.String DEVICE_PROVISIONED = "device_provisioned"; field public static final java.lang.String ENABLED_ACCESSIBILITY_SERVICES = "enabled_accessibility_services"; field public static final java.lang.String ENABLED_INPUT_METHODS = "enabled_input_methods"; + field public static final java.lang.String ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES = "enabled_notification_policy_access_packages"; field public static final deprecated java.lang.String HTTP_PROXY = "http_proxy"; field public static final java.lang.String INPUT_METHOD_SELECTOR_VISIBILITY = "input_method_selector_visibility"; field public static final java.lang.String INSTALL_NON_MARKET_APPS = "install_non_market_apps"; diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java index 3dc9582d6149..bf2e13af2820 100644 --- a/core/java/android/app/AlarmManager.java +++ b/core/java/android/app/AlarmManager.java @@ -611,7 +611,7 @@ public class AlarmManager { * * @hide */ - //@SystemApi + @SystemApi public void set(int type, long triggerAtMillis, long windowMillis, long intervalMillis, OnAlarmListener listener, Handler targetHandler, WorkSource workSource) { setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, null, diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java index e038a72ffa59..a73ad09d5f55 100644 --- a/core/java/android/app/Fragment.java +++ b/core/java/android/app/Fragment.java @@ -1410,7 +1410,8 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene /** * Called to do initial creation of a fragment. This is called after * {@link #onAttach(Activity)} and before - * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}, but is not called if the fragment + * instance is retained across Activity re-creation (see {@link #setRetainInstance(boolean)}). * * <p>Note that this can be called while the fragment's activity is * still in the process of being created. As such, you can not rely @@ -1649,8 +1650,10 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene } /** - * Called when the fragment is no longer attached to its activity. This - * is called after {@link #onDestroy()}. + * Called when the fragment is no longer attached to its activity. This is called after + * {@link #onDestroy()}, except in the cases where the fragment instance is retained across + * Activity re-creation (see {@link #setRetainInstance(boolean)}), in which case it is called + * after {@link #onStop()}. */ public void onDetach() { mCalled = true; diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index ffeb6ed32950..5a75640b3231 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -3823,6 +3823,9 @@ public class Notification implements Parcelable return this; } + /** @hide */ + public static final int MIN_ASHMEM_BITMAP_SIZE = 128 * (1 << 10); + /** * @hide */ @@ -3831,7 +3834,7 @@ public class Notification implements Parcelable super.purgeResources(); if (mPicture != null && mPicture.isMutable() && - mPicture.getAllocationByteCount() >= (128 * (1 << 10))) { + mPicture.getAllocationByteCount() >= MIN_ASHMEM_BITMAP_SIZE) { mPicture = mPicture.createAshmemBitmap(); } if (mBigLargeIcon != null) { diff --git a/core/java/android/app/backup/BackupHelper.java b/core/java/android/app/backup/BackupHelper.java index 7cbbbc307f7f..3074a8c89925 100644 --- a/core/java/android/app/backup/BackupHelper.java +++ b/core/java/android/app/backup/BackupHelper.java @@ -50,7 +50,11 @@ public interface BackupHelper { * new state after performing the backup operation. * <p class="note"> * <strong>Note:</strong> The helper should not close or seek either the {@code oldState} or - * the {@code newState} file descriptors.</p> + * the {@code newState} file descriptors. It is essential that when reading the helper's + * saved state from the {@code oldState} file, no extra content is consumed beyond + * what was stored by this helper. If more old state data is read, even accidentally, + * it will make it impossible for additional helpers that may be invoked after this one + * to properly reconstruct their prior state.</p> * * @param oldState An open, read-only {@link android.os.ParcelFileDescriptor} pointing to the * last backup state provided by the application. May be diff --git a/core/java/android/app/backup/BlobBackupHelper.java b/core/java/android/app/backup/BlobBackupHelper.java index cdc62dc4c912..82d0a94ce0da 100644 --- a/core/java/android/app/backup/BlobBackupHelper.java +++ b/core/java/android/app/backup/BlobBackupHelper.java @@ -20,7 +20,6 @@ import android.os.ParcelFileDescriptor; import android.util.ArrayMap; import android.util.Log; -import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -42,7 +41,7 @@ import java.util.zip.InflaterInputStream; */ public abstract class BlobBackupHelper implements BackupHelper { private static final String TAG = "BlobBackupHelper"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final boolean DEBUG = false; private final int mCurrentBlobVersion; private final String[] mKeys; @@ -92,16 +91,21 @@ public abstract class BlobBackupHelper implements BackupHelper { final ArrayMap<String, Long> state = new ArrayMap<String, Long>(); FileInputStream fis = new FileInputStream(oldStateFd.getFileDescriptor()); - BufferedInputStream bis = new BufferedInputStream(fis); - DataInputStream in = new DataInputStream(bis); + DataInputStream in = new DataInputStream(fis); try { int version = in.readInt(); if (version <= mCurrentBlobVersion) { final int numKeys = in.readInt(); + if (DEBUG) { + Log.i(TAG, " " + numKeys + " keys in state record"); + } for (int i = 0; i < numKeys; i++) { String key = in.readUTF(); long checksum = in.readLong(); + if (DEBUG) { + Log.i(TAG, " key '" + key + "' checksum is " + checksum); + } state.put(key, checksum); } } else { @@ -110,6 +114,9 @@ public abstract class BlobBackupHelper implements BackupHelper { } catch (EOFException e) { // Empty file is expected on first backup, so carry on. If the state // is truncated we just treat it the same way. + if (DEBUG) { + Log.i(TAG, "Hit EOF reading prior state"); + } state.clear(); } catch (Exception e) { Log.e(TAG, "Error examining prior backup state " + e.getMessage()); @@ -136,8 +143,13 @@ public abstract class BlobBackupHelper implements BackupHelper { final int N = (state != null) ? state.size() : 0; out.writeInt(N); for (int i = 0; i < N; i++) { - out.writeUTF(state.keyAt(i)); - out.writeLong(state.valueAt(i).longValue()); + final String key = state.keyAt(i); + final long checksum = state.valueAt(i).longValue(); + if (DEBUG) { + Log.i(TAG, " writing key " + key + " checksum = " + checksum); + } + out.writeUTF(key); + out.writeLong(checksum); } } catch (IOException e) { Log.e(TAG, "Unable to write updated state", e); @@ -226,6 +238,9 @@ public abstract class BlobBackupHelper implements BackupHelper { @Override public void performBackup(ParcelFileDescriptor oldStateFd, BackupDataOutput data, ParcelFileDescriptor newStateFd) { + if (DEBUG) { + Log.i(TAG, "Performing backup for " + this.getClass().getName()); + } final ArrayMap<String, Long> oldState = readOldState(oldStateFd); final ArrayMap<String, Long> newState = new ArrayMap<String, Long>(); @@ -234,12 +249,16 @@ public abstract class BlobBackupHelper implements BackupHelper { for (String key : mKeys) { final byte[] payload = deflate(getBackupPayload(key)); final long checksum = checksum(payload); + if (DEBUG) { + Log.i(TAG, "Key " + key + " backup checksum is " + checksum); + } newState.put(key, checksum); Long oldChecksum = oldState.get(key); - if (oldChecksum == null || checksum != oldChecksum) { + if (oldChecksum == null || checksum != oldChecksum.longValue()) { if (DEBUG) { - Log.i(TAG, "State has changed for key " + key + ", writing"); + Log.i(TAG, "Checksum has changed from " + oldChecksum + " to " + checksum + + " for key " + key + ", writing"); } if (payload != null) { data.writeEntityHeader(key, payload.length); @@ -258,7 +277,7 @@ public abstract class BlobBackupHelper implements BackupHelper { Log.w(TAG, "Unable to record notification state: " + e.getMessage()); newState.clear(); } finally { - // Always recommit the state even if nothing changed + // Always rewrite the state even if nothing changed writeBackupState(newState, newStateFd); } } @@ -291,6 +310,9 @@ public abstract class BlobBackupHelper implements BackupHelper { @Override public void writeNewStateDescription(ParcelFileDescriptor newState) { // Just ensure that we do a full backup the first time after a restore + if (DEBUG) { + Log.i(TAG, "Writing state description after restore"); + } writeBackupState(null, newState); } } diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 241e6db91756..159ca01d3f85 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -231,6 +231,7 @@ public final class DocumentsContract { * @see #FLAG_SUPPORTS_WRITE * @see #FLAG_SUPPORTS_DELETE * @see #FLAG_SUPPORTS_THUMBNAIL + * @see #FLAG_SUPPORTS_TYPED_DOCUMENT * @see #FLAG_DIR_PREFERS_GRID * @see #FLAG_DIR_PREFERS_LAST_MODIFIED */ @@ -347,6 +348,15 @@ public final class DocumentsContract { public static final int FLAG_SUPPORTS_MOVE = 1 << 8; /** + * Flag indicating that a document can be converted to alternative types. + * + * @see #COLUMN_FLAGS + * @see DocumentsProvider#openTypedDocument(String, String, Bundle, + * android.os.CancellationSignal) + */ + public static final int FLAG_SUPPORTS_TYPED_DOCUMENT = 1 << 9; + + /** * Flag indicating that document titles should be hidden when viewing * this directory in a larger format grid. For example, a directory * containing only images may want the image thumbnails to speak for diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index 28ac165b1003..f01073bbd43d 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -31,6 +31,7 @@ import static android.provider.DocumentsContract.getTreeDocumentId; import static android.provider.DocumentsContract.isTreeUri; import android.annotation.CallSuper; +import android.content.ClipDescription; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; @@ -502,6 +503,29 @@ public abstract class DocumentsProvider extends ContentProvider { } /** + * Open and return the document in a format matching the specified MIME + * type filter. + * <p> + * A provider may perform a conversion if the documents's MIME type is not + * matching the specified MIME type filter. + * + * @param documentId the document to return. + * @param mimeTypeFilter the MIME type filter for the requested format. May + * be *\/*, which matches any MIME type. + * @param opts extra options from the client. Specific to the content + * provider. + * @param signal used by the caller to signal if the request should be + * cancelled. May be null. + * @see Document#FLAG_SUPPORTS_TYPED_DOCUMENT + */ + @SuppressWarnings("unused") + public AssetFileDescriptor openTypedDocument( + String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) + throws FileNotFoundException { + throw new UnsupportedOperationException("Typed documents not supported"); + } + + /** * Implementation is provided by the parent class. Cannot be overriden. * * @see #queryRoots(String[]) @@ -846,34 +870,50 @@ public abstract class DocumentsProvider extends ContentProvider { * Implementation is provided by the parent class. Cannot be overriden. * * @see #openDocumentThumbnail(String, Point, CancellationSignal) + * @see #openTypedDocument(String, String, Bundle, CancellationSignal) */ @Override public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) throws FileNotFoundException { - enforceTree(uri); - if (opts != null && opts.containsKey(ContentResolver.EXTRA_SIZE)) { - final Point sizeHint = opts.getParcelable(ContentResolver.EXTRA_SIZE); - return openDocumentThumbnail(getDocumentId(uri), sizeHint, null); - } else { - return super.openTypedAssetFile(uri, mimeTypeFilter, opts); - } + return openTypedAssetFileImpl(uri, mimeTypeFilter, opts, null); } /** * Implementation is provided by the parent class. Cannot be overriden. * * @see #openDocumentThumbnail(String, Point, CancellationSignal) + * @see #openTypedDocument(String, String, Bundle, CancellationSignal) */ @Override public final AssetFileDescriptor openTypedAssetFile( Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) throws FileNotFoundException { + return openTypedAssetFileImpl(uri, mimeTypeFilter, opts, signal); + } + + /** + * @hide + */ + private final AssetFileDescriptor openTypedAssetFileImpl( + Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) + throws FileNotFoundException { enforceTree(uri); + final String documentId = getDocumentId(uri); if (opts != null && opts.containsKey(ContentResolver.EXTRA_SIZE)) { final Point sizeHint = opts.getParcelable(ContentResolver.EXTRA_SIZE); - return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal); - } else { - return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal); + return openDocumentThumbnail(documentId, sizeHint, signal); + } + if ("*/*".equals(mimeTypeFilter)) { + // If they can take anything, the untyped open call is good enough. + return openAssetFile(uri, "r"); + } + final String baseType = getType(uri); + if (baseType != null && ClipDescription.compareMimeTypes(baseType, mimeTypeFilter)) { + // Use old untyped open call if this provider has a type for this + // URI and it matches the request. + return openAssetFile(uri, "r"); } + // For any other yet unhandled case, let the provider subclass handle it. + return openTypedDocument(documentId, mimeTypeFilter, opts, signal); } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ad46c3dadbd3..37f2c0477887 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5598,8 +5598,6 @@ public final class Settings { /** * Names of the packages that the current user has explicitly allowed to * manage notification policy configuration, separated by ':'. - * - * @hide */ public static final String ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES = "enabled_notification_policy_access_packages"; diff --git a/core/java/android/service/notification/ConditionProviderService.java b/core/java/android/service/notification/ConditionProviderService.java index bbac023b9a38..88bd283d06a1 100644 --- a/core/java/android/service/notification/ConditionProviderService.java +++ b/core/java/android/service/notification/ConditionProviderService.java @@ -168,11 +168,6 @@ public abstract class ConditionProviderService extends Service { } @Override - public void onRequestConditions(int relevance) { - mHandler.obtainMessage(H.ON_REQUEST_CONDITIONS, relevance, 0).sendToTarget(); - } - - @Override public void onSubscribe(Uri conditionId) { mHandler.obtainMessage(H.ON_SUBSCRIBE, conditionId).sendToTarget(); } @@ -185,7 +180,6 @@ public abstract class ConditionProviderService extends Service { private final class H extends Handler { private static final int ON_CONNECTED = 1; - private static final int ON_REQUEST_CONDITIONS = 2; private static final int ON_SUBSCRIBE = 3; private static final int ON_UNSUBSCRIBE = 4; @@ -198,10 +192,6 @@ public abstract class ConditionProviderService extends Service { name = "onConnected"; onConnected(); break; - case ON_REQUEST_CONDITIONS: - name = "onRequestConditions"; - onRequestConditions(msg.arg1); - break; case ON_SUBSCRIBE: name = "onSubscribe"; onSubscribe((Uri)msg.obj); diff --git a/core/java/android/service/notification/IConditionProvider.aidl b/core/java/android/service/notification/IConditionProvider.aidl index ada8939b01f2..3f3c6b80286d 100644 --- a/core/java/android/service/notification/IConditionProvider.aidl +++ b/core/java/android/service/notification/IConditionProvider.aidl @@ -22,7 +22,6 @@ import android.service.notification.Condition; /** @hide */ oneway interface IConditionProvider { void onConnected(); - void onRequestConditions(int relevance); void onSubscribe(in Uri conditionId); void onUnsubscribe(in Uri conditionId); }
\ No newline at end of file diff --git a/graphics/java/android/graphics/drawable/Icon.java b/graphics/java/android/graphics/drawable/Icon.java index 44d7530c6c11..d800acbe805e 100644 --- a/graphics/java/android/graphics/drawable/Icon.java +++ b/graphics/java/android/graphics/drawable/Icon.java @@ -377,6 +377,9 @@ public final class Icon implements Parcelable { return loadDrawable(context); } + /** @hide */ + public static final int MIN_ASHMEM_ICON_SIZE = 128 * (1 << 10); + /** * Puts the memory used by this instance into Ashmem memory, if possible. * @hide @@ -384,7 +387,7 @@ public final class Icon implements Parcelable { public void convertToAshmem() { if (mType == TYPE_BITMAP && getBitmap().isMutable() && - getBitmap().getAllocationByteCount() >= (128 * (1 << 10))) { + getBitmap().getAllocationByteCount() >= MIN_ASHMEM_ICON_SIZE) { setBitmap(getBitmap().createAshmemBitmap()); } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java index e3b13246090b..0ee970d6db3a 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java @@ -16,11 +16,11 @@ package com.android.documentsui; -import static com.android.documentsui.DirectoryFragment.ANIM_DOWN; -import static com.android.documentsui.DirectoryFragment.ANIM_NONE; -import static com.android.documentsui.DirectoryFragment.ANIM_SIDE; -import static com.android.documentsui.DirectoryFragment.ANIM_UP; import static com.android.documentsui.Shared.DEBUG; +import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_DOWN; +import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_NONE; +import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_SIDE; +import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_UP; import static com.android.internal.util.Preconditions.checkArgument; import android.app.Activity; @@ -55,6 +55,7 @@ import android.widget.SearchView.OnQueryTextListener; import android.widget.TextView; import com.android.documentsui.RecentsProvider.ResumeColumns; +import com.android.documentsui.dirlist.DirectoryFragment; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.DurableUtils; @@ -69,7 +70,7 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; -abstract class BaseActivity extends Activity { +public abstract class BaseActivity extends Activity { static final String EXTRA_STATE = "state"; @@ -383,7 +384,7 @@ abstract class BaseActivity extends Activity { invalidateOptionsMenu(); } - void onStateChanged() { + public void onStateChanged() { invalidateOptionsMenu(); } @@ -421,7 +422,7 @@ abstract class BaseActivity extends Activity { super.onRestoreInstanceState(state); } - RootInfo getCurrentRoot() { + public RootInfo getCurrentRoot() { if (mState.stack.root != null) { return mState.stack.root; } else { @@ -825,7 +826,7 @@ abstract class BaseActivity extends Activity { * Interface providing access to current view of documents * even when all documents are not homed to the same parent. */ - interface DocumentContext { + public interface DocumentContext { /** * Returns the cursor for the selected document. The cursor can be used to retrieve * details about a document and its siblings. diff --git a/packages/DocumentsUI/src/com/android/documentsui/CopyService.java b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java index 047949f54ad8..c62f61945b58 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/CopyService.java +++ b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java @@ -410,6 +410,7 @@ public class CopyService extends IntentService { * * @param srcInfo DocumentInfos for the documents to copy. * @param dstDirInfo The destination directory. + * @param mode The transfer mode (copy or move). * @throws RemoteException */ private void copy(DocumentInfo srcInfo, DocumentInfo dstDirInfo, int mode) @@ -417,11 +418,37 @@ public class CopyService extends IntentService { if (DEBUG) Log.d(TAG, "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")" + " to " + dstDirInfo.displayName + " (" + dstDirInfo.derivedUri + ")"); + // When copying within the same provider, try to use optimized copying and moving. + // If not supported, then fallback to byte-by-byte copy/move. + if (srcInfo.authority.equals(dstDirInfo.authority)) { + switch (mode) { + case TRANSFER_MODE_COPY: + if ((srcInfo.flags & Document.FLAG_SUPPORTS_COPY) != 0) { + if (DocumentsContract.copyDocument(mSrcClient, srcInfo.derivedUri, + dstDirInfo.derivedUri) == null) { + mFailedFiles.add(srcInfo); + } + return; + } + break; + case TRANSFER_MODE_MOVE: + if ((srcInfo.flags & Document.FLAG_SUPPORTS_MOVE) != 0) { + if (DocumentsContract.moveDocument(mSrcClient, srcInfo.derivedUri, + dstDirInfo.derivedUri) == null) { + mFailedFiles.add(srcInfo); + } + return; + } + break; + default: + throw new IllegalArgumentException("Unknown transfer mode."); + } + } + final Uri dstUri = DocumentsContract.createDocument(mDstClient, dstDirInfo.derivedUri, srcInfo.mimeType, srcInfo.displayName); if (dstUri == null) { // If this is a directory, the entire subdir will not be copied over. - Log.e(TAG, "Error while copying " + srcInfo.displayName); mFailedFiles.add(srcInfo); return; } @@ -522,8 +549,6 @@ public class CopyService extends IntentService { try { DocumentInfo info = DocumentInfo.fromUri(getContentResolver(), srcUri); mFailedFiles.add(info); - Log.e(TAG, "Error while copying " + info.displayName + " (" + info.derivedUri + ")", - copyError); } catch (FileNotFoundException ignore) { // Generate a dummy DocumentInfo so an error still gets reflected in the UI for this // file. @@ -531,7 +556,6 @@ public class CopyService extends IntentService { info.derivedUri = srcUri; info.displayName = "Unknown [" + srcUri + "]"; mFailedFiles.add(info); - Log.e(TAG, "Error while copying " + srcUri, copyError); } if (dstFile != null) { diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java index bb82b386aa12..b0bbec3e5919 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java @@ -17,11 +17,9 @@ package com.android.documentsui; import static com.android.documentsui.Shared.TAG; -import static com.android.documentsui.State.MODE_UNKNOWN; import static com.android.documentsui.State.SORT_ORDER_DISPLAY_NAME; import static com.android.documentsui.State.SORT_ORDER_LAST_MODIFIED; import static com.android.documentsui.State.SORT_ORDER_SIZE; -import static com.android.documentsui.State.SORT_ORDER_UNKNOWN; import static com.android.documentsui.model.DocumentInfo.getCursorInt; import android.content.AsyncTaskLoader; @@ -38,6 +36,7 @@ import android.provider.DocumentsContract.Document; import android.util.Log; import com.android.documentsui.RecentsProvider.StateColumns; +import com.android.documentsui.dirlist.DirectoryFragment; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.RootInfo; @@ -45,23 +44,6 @@ import libcore.io.IoUtils; import java.io.FileNotFoundException; -class DirectoryResult implements AutoCloseable { - ContentProviderClient client; - Cursor cursor; - Exception exception; - - int mode = MODE_UNKNOWN; - int sortOrder = SORT_ORDER_UNKNOWN; - - @Override - public void close() { - IoUtils.closeQuietly(cursor); - ContentProviderClient.releaseQuietly(client); - cursor = null; - client = null; - } -} - public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { private static final String[] SEARCH_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR }; diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryResult.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryResult.java new file mode 100644 index 000000000000..e7e4f737a896 --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryResult.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +import static com.android.documentsui.State.MODE_UNKNOWN; +import static com.android.documentsui.State.SORT_ORDER_UNKNOWN; + +import android.content.ContentProviderClient; +import android.database.Cursor; + +import libcore.io.IoUtils; + +public class DirectoryResult implements AutoCloseable { + ContentProviderClient client; + public Cursor cursor; + public Exception exception; + + public int mode = MODE_UNKNOWN; + public int sortOrder = SORT_ORDER_UNKNOWN; + + @Override + public void close() { + IoUtils.closeQuietly(cursor); + ContentProviderClient.releaseQuietly(client); + cursor = null; + client = null; + } +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java index 6ba07fbbaf6a..b3c28469e71d 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java @@ -39,7 +39,7 @@ import java.util.List; * ClipboardManager wrapper class providing higher level logical * support for dealing with Documents. */ -final class DocumentClipper { +public final class DocumentClipper { private static final String TAG = "DocumentClipper"; diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java index 18957ee66e38..403a464fb51b 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java @@ -16,12 +16,12 @@ package com.android.documentsui; -import static com.android.documentsui.DirectoryFragment.ANIM_NONE; import static com.android.documentsui.State.ACTION_CREATE; import static com.android.documentsui.State.ACTION_GET_CONTENT; import static com.android.documentsui.State.ACTION_OPEN; import static com.android.documentsui.State.ACTION_OPEN_COPY_DESTINATION; import static com.android.documentsui.State.ACTION_OPEN_TREE; +import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_NONE; import android.app.Activity; import android.app.Fragment; @@ -53,6 +53,7 @@ import android.widget.Toolbar; import com.android.documentsui.RecentsProvider.RecentColumns; import com.android.documentsui.RecentsProvider.ResumeColumns; +import com.android.documentsui.dirlist.DirectoryFragment; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DurableUtils; import com.android.documentsui.model.RootInfo; diff --git a/packages/DocumentsUI/src/com/android/documentsui/Events.java b/packages/DocumentsUI/src/com/android/documentsui/Events.java index d4c3ba3ba3c3..49dae3d0402f 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/Events.java +++ b/packages/DocumentsUI/src/com/android/documentsui/Events.java @@ -25,33 +25,33 @@ import android.view.View; /** * Utility code for dealing with MotionEvents. */ -final class Events { +public final class Events { /** * Returns true if event was triggered by a mouse. */ - static boolean isMouseEvent(MotionEvent e) { + public static boolean isMouseEvent(MotionEvent e) { return isMouseType(e.getToolType(0)); } /** * Returns true if event was triggered by a finger or stylus touch. */ - static boolean isTouchEvent(MotionEvent e) { + public static boolean isTouchEvent(MotionEvent e) { return isTouchType(e.getToolType(0)); } /** * Returns true if event was triggered by a mouse. */ - static boolean isMouseType(int toolType) { + public static boolean isMouseType(int toolType) { return toolType == MotionEvent.TOOL_TYPE_MOUSE; } /** * Returns true if event was triggered by a finger or stylus touch. */ - static boolean isTouchType(int toolType) { + public static boolean isTouchType(int toolType) { return toolType == MotionEvent.TOOL_TYPE_FINGER || toolType == MotionEvent.TOOL_TYPE_STYLUS; } @@ -59,28 +59,28 @@ final class Events { /** * Returns true if event was triggered by a finger or stylus touch. */ - static boolean isActionDown(MotionEvent e) { + public static boolean isActionDown(MotionEvent e) { return e.getActionMasked() == MotionEvent.ACTION_DOWN; } /** * Returns true if event was triggered by a finger or stylus touch. */ - static boolean isActionUp(MotionEvent e) { + public static boolean isActionUp(MotionEvent e) { return e.getActionMasked() == MotionEvent.ACTION_UP; } /** * Returns true if the shift is pressed. */ - boolean isShiftPressed(MotionEvent e) { + public boolean isShiftPressed(MotionEvent e) { return hasShiftBit(e.getMetaState()); } /** * Returns true if the "SHIFT" bit is set. */ - static boolean hasShiftBit(int metaState) { + public static boolean hasShiftBit(int metaState) { return (metaState & KeyEvent.META_SHIFT_ON) != 0; } @@ -88,7 +88,7 @@ final class Events { * A facade over MotionEvent primarily designed to permit for unit testing * of related code. */ - interface InputEvent { + public interface InputEvent { boolean isMouseEvent(); boolean isPrimaryButtonPressed(); boolean isSecondaryButtonPressed(); @@ -109,7 +109,7 @@ final class Events { int getItemPosition(); } - static final class MotionInputEvent implements InputEvent { + public static final class MotionInputEvent implements InputEvent { private final MotionEvent mEvent; private final RecyclerView mView; private final int mPosition; diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java index 627ba756680f..c7cffed91a96 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java @@ -16,8 +16,8 @@ package com.android.documentsui; -import static com.android.documentsui.DirectoryFragment.ANIM_NONE; import static com.android.documentsui.Shared.DEBUG; +import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_NONE; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkState; @@ -44,6 +44,7 @@ import android.widget.Spinner; import android.widget.Toolbar; import com.android.documentsui.RecentsProvider.ResumeColumns; +import com.android.documentsui.dirlist.DirectoryFragment; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.DurableUtils; diff --git a/packages/DocumentsUI/src/com/android/documentsui/ManageRootActivity.java b/packages/DocumentsUI/src/com/android/documentsui/ManageRootActivity.java index 26a373403d80..3045fa847b26 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/ManageRootActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/ManageRootActivity.java @@ -16,8 +16,8 @@ package com.android.documentsui; -import static com.android.documentsui.DirectoryFragment.ANIM_NONE; import static com.android.documentsui.State.ACTION_MANAGE; +import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_NONE; import android.app.Activity; import android.app.Fragment; @@ -41,6 +41,7 @@ import android.widget.Spinner; import android.widget.Toolbar; import com.android.documentsui.RecentsProvider.ResumeColumns; +import com.android.documentsui.dirlist.DirectoryFragment; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DurableUtils; import com.android.documentsui.model.RootInfo; diff --git a/packages/DocumentsUI/src/com/android/documentsui/MessageBar.java b/packages/DocumentsUI/src/com/android/documentsui/MessageBar.java index 312d53b9dc1c..5c6213fdc933 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/MessageBar.java +++ b/packages/DocumentsUI/src/com/android/documentsui/MessageBar.java @@ -105,7 +105,7 @@ public class MessageBar extends Fragment { return mView; } - void hide() { + public void hide() { // The container view is used to show/hide the error bar. If a container is not provided, // fall back to showing/hiding the error bar View, which also works, but does not provide // the same animated transition. @@ -116,7 +116,7 @@ public class MessageBar extends Fragment { } } - void show() { + public void show() { // The container view is used to show/hide the error bar. If a container is not provided, // fall back to showing/hiding the error bar View, which also works, but does not provide // the same animated transition. diff --git a/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java b/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java index f48b298a4f2f..48c1a733703c 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java +++ b/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java @@ -22,7 +22,7 @@ import android.app.Activity; import android.support.design.widget.Snackbar; import android.view.View; -final class Snackbars { +public final class Snackbars { private Snackbars() {} public static final Snackbar makeSnackbar(Activity activity, int messageId, int duration) { diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java index 3b7da78ccb8d..e1f5ee16d29b 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.documentsui; +package com.android.documentsui.dirlist; import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.State.ACTION_CREATE; @@ -88,11 +88,44 @@ import android.view.ViewParent; import android.widget.ImageView; import android.widget.TextView; +import com.android.documentsui.BaseActivity; +import com.android.documentsui.CopyService; +import com.android.documentsui.DirectoryLoader; +import com.android.documentsui.DirectoryResult; +import com.android.documentsui.DocumentClipper; +import com.android.documentsui.DocumentsActivity; +import com.android.documentsui.DocumentsApplication; +import com.android.documentsui.Events; +import com.android.documentsui.IconUtils; +import com.android.documentsui.Menus; +import com.android.documentsui.MessageBar; +import com.android.documentsui.MimePredicate; +import com.android.documentsui.ProviderExecutor; +import com.android.documentsui.R; +import com.android.documentsui.RecentLoader; +import com.android.documentsui.RecentsProvider; +import com.android.documentsui.RootCursorWrapper; +import com.android.documentsui.RootsCache; +import com.android.documentsui.Shared; +import com.android.documentsui.Snackbars; +import com.android.documentsui.State; +import com.android.documentsui.ThumbnailCache; import com.android.documentsui.BaseActivity.DocumentContext; -import com.android.documentsui.MultiSelectManager.Selection; +import com.android.documentsui.BaseActivity.DocumentsIntent; import com.android.documentsui.ProviderExecutor.Preemptable; +import com.android.documentsui.R.animator; +import com.android.documentsui.R.attr; +import com.android.documentsui.R.bool; +import com.android.documentsui.R.dimen; +import com.android.documentsui.R.drawable; +import com.android.documentsui.R.id; +import com.android.documentsui.R.layout; +import com.android.documentsui.R.menu; +import com.android.documentsui.R.plurals; +import com.android.documentsui.R.string; import com.android.documentsui.RecentsProvider.StateColumns; -import com.android.documentsui.dirlist.FragmentTuner; +import com.android.documentsui.dirlist.MultiSelectManager.Callback; +import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.RootInfo; @@ -1391,7 +1424,7 @@ public class DirectoryFragment extends Fragment { return clipData; } - void copySelectedToClipboard() { + public void copySelectedToClipboard() { Selection sel = mSelectionManager.getSelection(new Selection()); copySelectionToClipboard(sel); } @@ -1410,7 +1443,7 @@ public class DirectoryFragment extends Fragment { }.execute(items); } - void pasteFromClipboard() { + public void pasteFromClipboard() { copyFromClipboard(); getActivity().invalidateOptionsMenu(); } @@ -1440,7 +1473,7 @@ public class DirectoryFragment extends Fragment { return dest != null && dest.isDirectory() && dest.isCreateSupported(); } - void selectAllFiles() { + public void selectAllFiles() { boolean changed = mSelectionManager.setItemsSelected(0, mModel.getItemCount(), true); if (changed) { updateDisplayState(); diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryItemAnimator.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryItemAnimator.java index 0eb1ea55895e..0963845db628 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryItemAnimator.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryItemAnimator.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.documentsui; +package com.android.documentsui.dirlist; import android.animation.Animator; import android.animation.ArgbEvaluator; diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java index ca85cff8a32b..7e9bbe2023d6 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java @@ -24,7 +24,6 @@ import android.os.SystemProperties; import android.view.Menu; import android.view.MenuItem; -import com.android.documentsui.DirectoryFragment; import com.android.documentsui.Menus; import com.android.documentsui.R; import com.android.documentsui.State; diff --git a/packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java index 4fde6ff74fea..b5a3b9374d10 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.documentsui; +package com.android.documentsui.dirlist; import static com.android.documentsui.Shared.DEBUG; import static com.android.internal.util.Preconditions.checkArgument; @@ -41,8 +41,11 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import com.android.documentsui.Events; +import com.android.documentsui.R; import com.android.documentsui.Events.InputEvent; import com.android.documentsui.Events.MotionInputEvent; +import com.android.documentsui.R.drawable; import java.util.ArrayList; import java.util.Collections; @@ -342,7 +345,7 @@ public final class MultiSelectManager implements View.OnKeyListener { * * @param position */ - void toggleSelection(int position) { + public void toggleSelection(int position) { // Position may be special "no position" during certain // transitional phases. If so, skip handling of the event. if (position == RecyclerView.NO_POSITION) { @@ -801,14 +804,11 @@ public final class MultiSelectManager implements View.OnKeyListener { cancelProvisionalSelection(); } - /** @hide */ - @VisibleForTesting - void clear() { + public void clear() { mSavedSelection.clear(); mTotalSelection.clear(); } - /** @hide */ @VisibleForTesting void copyFrom(Selection source) { mSavedSelection = source.mSavedSelection.clone(); diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/TestInputEvent.java b/packages/DocumentsUI/tests/src/com/android/documentsui/TestInputEvent.java index e83f9e007349..ec5321a8c940 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/TestInputEvent.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/TestInputEvent.java @@ -3,7 +3,7 @@ package com.android.documentsui; import android.graphics.Point; import android.support.v7.widget.RecyclerView; -class TestInputEvent implements Events.InputEvent { +public class TestInputEvent implements Events.InputEvent { public boolean mouseEvent; public boolean primaryButtonPressed; diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/DirectoryFragmentModelTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DirectoryFragmentModelTest.java index 36d880a06892..746e2117dc02 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/DirectoryFragmentModelTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DirectoryFragmentModelTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.documentsui; +package com.android.documentsui.dirlist; import android.content.ContentResolver; import android.content.Context; @@ -27,8 +27,9 @@ import android.test.AndroidTestCase; import android.test.mock.MockContentResolver; import android.view.ViewGroup; -import com.android.documentsui.DirectoryFragment.Model; -import com.android.documentsui.MultiSelectManager.Selection; +import com.android.documentsui.DirectoryResult; +import com.android.documentsui.dirlist.DirectoryFragment.Model; +import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.model.DocumentInfo; import java.util.List; diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java index ceb8cdc34faa..24f5c9e21f7a 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.documentsui; +package com.android.documentsui.dirlist; import android.support.v7.widget.RecyclerView; import android.test.AndroidTestCase; @@ -22,7 +22,9 @@ import android.util.SparseBooleanArray; import android.view.View; import android.view.ViewGroup; -import com.android.documentsui.MultiSelectManager.Selection; +import com.android.documentsui.TestInputEvent; +import com.android.documentsui.dirlist.MultiSelectManager; +import com.android.documentsui.dirlist.MultiSelectManager.Selection; import org.mockito.Mockito; diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManager_GridModelTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java index f6683fa840e2..c4b6ce5c7f87 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManager_GridModelTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java @@ -14,10 +14,7 @@ * limitations under the License. */ -package com.android.documentsui; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +package com.android.documentsui.dirlist; import android.graphics.Point; import android.graphics.Rect; @@ -26,7 +23,7 @@ import android.test.AndroidTestCase; import android.util.SparseBooleanArray; import android.view.View; -import com.android.documentsui.MultiSelectManager.GridModel; +import com.android.documentsui.dirlist.MultiSelectManager.GridModel; public class MultiSelectManager_GridModelTest extends AndroidTestCase { diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManager_SelectionTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SelectionTest.java index eddf4ef6fede..64da750b1306 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManager_SelectionTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SelectionTest.java @@ -14,13 +14,11 @@ * limitations under the License. */ -package com.android.documentsui; - -import static org.junit.Assert.*; +package com.android.documentsui.dirlist; import android.test.AndroidTestCase; -import com.android.documentsui.MultiSelectManager.Selection; +import com.android.documentsui.dirlist.MultiSelectManager.Selection; public class MultiSelectManager_SelectionTest extends AndroidTestCase{ diff --git a/packages/SettingsLib/Android.mk b/packages/SettingsLib/Android.mk index 0245ed37af2e..c8606686749d 100644 --- a/packages/SettingsLib/Android.mk +++ b/packages/SettingsLib/Android.mk @@ -3,6 +3,8 @@ include $(CLEAR_VARS) LOCAL_MODULE := SettingsLib +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 + LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res LOCAL_SRC_FILES := $(call all-java-files-under, src) diff --git a/packages/SettingsLib/res/drawable/ic_menu.xml b/packages/SettingsLib/res/drawable/ic_menu.xml new file mode 100644 index 000000000000..910a3d08b3ba --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_menu.xml @@ -0,0 +1,24 @@ +<!-- + Copyright (C) 2015 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M3.0,18.0l18.0,0.0l0.0,-2.0L3.0,16.0l0.0,2.0zm0.0,-5.0l18.0,0.0l0.0,-2.0L3.0,11.0l0.0,2.0zm0.0,-7.0l0.0,2.0l18.0,0.0L21.0,6.0L3.0,6.0z"/> +</vector> diff --git a/packages/SettingsLib/res/layout/drawer_category.xml b/packages/SettingsLib/res/layout/drawer_category.xml new file mode 100644 index 000000000000..20afcd48db7a --- /dev/null +++ b/packages/SettingsLib/res/layout/drawer_category.xml @@ -0,0 +1,36 @@ +<!-- + Copyright (C) 2015 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?android:attr/listDivider" /> + + <TextView + android:id="@android:id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:paddingStart="16dp" + android:textAppearance="?android:attr/textAppearanceSmall" /> + +</LinearLayout> diff --git a/packages/SettingsLib/res/layout/drawer_item.xml b/packages/SettingsLib/res/layout/drawer_item.xml new file mode 100644 index 000000000000..4b53049fe9b1 --- /dev/null +++ b/packages/SettingsLib/res/layout/drawer_item.xml @@ -0,0 +1,40 @@ +<!-- + Copyright (C) 2015 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/tile_item" + android:layout_width="match_parent" + android:layout_height="48dp" + android:orientation="horizontal" > + + <ImageView + android:id="@android:id/icon" + android:layout_width="72dp" + android:layout_height="24dp" + android:layout_gravity="center_vertical" + android:tint="?android:attr/colorAccent" + android:paddingStart="16dp" + android:paddingEnd="32dp" /> + + <TextView + android:id="@android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:textColor="?android:attr/colorControlNormal" + android:textAppearance="?android:attr/textAppearanceMedium" /> + +</LinearLayout> diff --git a/packages/SettingsLib/res/layout/settings_with_drawer.xml b/packages/SettingsLib/res/layout/settings_with_drawer.xml new file mode 100644 index 000000000000..a9a78e80495d --- /dev/null +++ b/packages/SettingsLib/res/layout/settings_with_drawer.xml @@ -0,0 +1,55 @@ +<!-- + Copyright (C) 2015 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<android.support.v4.widget.DrawerLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/drawer_layout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <!-- The main content view --> + <LinearLayout + android:id="@+id/content_parent" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + <FrameLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="?android:attr/actionBarStyle"> + <Toolbar + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/action_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:navigationContentDescription="@*android:string/action_bar_up_description" + android:theme="?android:attr/actionBarTheme" + style="?android:attr/toolbarStyle" + android:background="?android:attr/colorPrimary" /> + </FrameLayout> + <FrameLayout + android:id="@+id/content_frame" + android:layout_width="match_parent" + android:layout_height="fill_parent" /> + </LinearLayout> + <!-- The navigation drawer --> + <ListView android:id="@+id/left_drawer" + android:layout_width="300dp" + android:layout_height="match_parent" + android:layout_gravity="start" + android:choiceMode="singleChoice" + android:divider="@android:color/transparent" + android:dividerHeight="0dp" + android:background="?android:attr/colorBackground" /> +</android.support.v4.widget.DrawerLayout> diff --git a/packages/SettingsLib/res/layout/user_preference.xml b/packages/SettingsLib/res/layout/user_preference.xml new file mode 100644 index 000000000000..75031fde3733 --- /dev/null +++ b/packages/SettingsLib/res/layout/user_preference.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/widget_frame" + android:layout_width="match_parent" + android:layout_height="@dimen/user_spinner_item_height" + android:paddingStart="@dimen/user_spinner_padding_sides" + android:paddingEnd="@dimen/user_spinner_padding_sides" + android:orientation="horizontal" > + + <ImageView + android:id="@+android:id/icon" + android:layout_width="@dimen/user_icon_view_height" + android:layout_height="@dimen/user_icon_view_height" + android:layout_gravity="center" + android:scaleType="fitCenter" + android:paddingBottom="@dimen/user_spinner_padding" + android:paddingTop="@dimen/user_spinner_padding" /> + + <TextView + android:id="@+android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:layout_gravity="center" + android:labelFor="@+android:id/icon" + android:ellipsize="marquee" + android:fadingEdge="horizontal" + android:paddingStart="@dimen/user_spinner_padding" + android:paddingEnd="@dimen/user_spinner_padding" + style="?android:attr/textAppearanceMedium" /> + +</LinearLayout> diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml index 3ad8f2122cb2..d7c78f6ffa6e 100644 --- a/packages/SettingsLib/res/values/dimens.xml +++ b/packages/SettingsLib/res/values/dimens.xml @@ -23,4 +23,12 @@ <dimen name="disappear_y_translation">-32dp</dimen> <dimen name="circle_avatar_size">40dp</dimen> + + <!-- Height of a user icon view --> + <dimen name="user_icon_view_height">56dp</dimen> + <!-- User spinner --> + <dimen name="user_spinner_height">72dp</dimen> + <dimen name="user_spinner_padding">4dp</dimen> + <dimen name="user_spinner_padding_sides">20dp</dimen> + <dimen name="user_spinner_item_height">56dp</dimen> </resources> diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 14eb084ce4c6..9e48849226a3 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -340,4 +340,12 @@ <item>kor</item> </string-array> + <!-- Title for profile selection dialog [CHAR LIMIT=30] --> + <string name="choose_profile">Choose Profile</string> + + <!-- Header for items under the personal user [CHAR LIMIT=30] --> + <string name="category_personal">Personal</string> + <!-- Header for items under the work user [CHAR LIMIT=30] --> + <string name="category_work">Work</string> + </resources> diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java new file mode 100644 index 000000000000..0f322cf6912d --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.drawer; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +public class DashboardCategory implements Parcelable { + + /** + * Title of the category that is shown to the user. + */ + public CharSequence title; + + /** + * Key used for placing external tiles. + */ + public String key; + + /** + * Used to control display order. + */ + public int priority; + + /** + * List of the category's children + */ + public List<DashboardTile> tiles = new ArrayList<DashboardTile>(); + + + public DashboardCategory() { + // Empty + } + + public void addTile(DashboardTile tile) { + tiles.add(tile); + } + + public void addTile(int n, DashboardTile tile) { + tiles.add(n, tile); + } + + public void removeTile(DashboardTile tile) { + tiles.remove(tile); + } + + public void removeTile(int n) { + tiles.remove(n); + } + + public int getTilesCount() { + return tiles.size(); + } + + public DashboardTile getTile(int n) { + return tiles.get(n); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + TextUtils.writeToParcel(title, dest, flags); + dest.writeString(key); + dest.writeInt(priority); + + final int count = tiles.size(); + dest.writeInt(count); + + for (int n = 0; n < count; n++) { + DashboardTile tile = tiles.get(n); + tile.writeToParcel(dest, flags); + } + } + + public void readFromParcel(Parcel in) { + title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + key = in.readString(); + priority = in.readInt(); + + final int count = in.readInt(); + + for (int n = 0; n < count; n++) { + DashboardTile tile = DashboardTile.CREATOR.createFromParcel(in); + tiles.add(tile); + } + } + + DashboardCategory(Parcel in) { + readFromParcel(in); + } + + public static final Creator<DashboardCategory> CREATOR = new Creator<DashboardCategory>() { + public DashboardCategory createFromParcel(Parcel source) { + return new DashboardCategory(source); + } + + public DashboardCategory[] newArray(int size) { + return new DashboardCategory[size]; + } + }; +} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardTile.java b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardTile.java new file mode 100644 index 000000000000..6fef134f452c --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardTile.java @@ -0,0 +1,142 @@ +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.drawer; + +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.UserHandle; +import android.text.TextUtils; + +import java.util.ArrayList; + +/** + * Description of a single dashboard tile that the user can select. + */ +public class DashboardTile implements Parcelable { + + /** + * Title of the tile that is shown to the user. + * @attr ref android.R.styleable#PreferenceHeader_title + */ + public CharSequence title; + + /** + * Optional summary describing what this tile controls. + * @attr ref android.R.styleable#PreferenceHeader_summary + */ + public CharSequence summary; + + /** + * Optional icon to show for this tile. + * @attr ref android.R.styleable#PreferenceHeader_icon + */ + public Icon icon; + + /** + * Intent to launch when the preference is selected. + */ + public Intent intent; + + /** + * Optional list of user handles which the intent should be launched on. + */ + public ArrayList<UserHandle> userHandle = new ArrayList<>(); + + /** + * Optional additional data for use by subclasses of the activity + */ + public Bundle extras; + + /** + * Category in which the tile should be placed. + */ + public String category; + + /** + * Priority of the intent filter that created this tile, used for display ordering. + */ + public int priority; + + public DashboardTile() { + // Empty + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + TextUtils.writeToParcel(title, dest, flags); + TextUtils.writeToParcel(summary, dest, flags); + if (icon != null) { + dest.writeByte((byte) 1); + icon.writeToParcel(dest, flags); + } else { + dest.writeByte((byte) 0); + } + if (intent != null) { + dest.writeByte((byte) 1); + intent.writeToParcel(dest, flags); + } else { + dest.writeByte((byte) 0); + } + final int N = userHandle.size(); + dest.writeInt(N); + for (int i = 0; i < N; i++) { + userHandle.get(i).writeToParcel(dest, flags); + } + dest.writeBundle(extras); + dest.writeString(category); + dest.writeInt(priority); + } + + public void readFromParcel(Parcel in) { + title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + if (in.readByte() != 0) { + icon = Icon.CREATOR.createFromParcel(in); + } + if (in.readByte() != 0) { + intent = Intent.CREATOR.createFromParcel(in); + } + final int N = in.readInt(); + for (int i = 0; i < N; i++) { + userHandle.add(UserHandle.CREATOR.createFromParcel(in)); + } + extras = in.readBundle(); + category = in.readString(); + priority = in.readInt(); + } + + DashboardTile(Parcel in) { + readFromParcel(in); + } + + public static final Creator<DashboardTile> CREATOR = new Creator<DashboardTile>() { + public DashboardTile createFromParcel(Parcel source) { + return new DashboardTile(source); + } + public DashboardTile[] newArray(int size) { + return new DashboardTile[size]; + } + }; +} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/ProfileSelectDialog.java b/packages/SettingsLib/src/com/android/settingslib/drawer/ProfileSelectDialog.java new file mode 100644 index 000000000000..793e877dba24 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/ProfileSelectDialog.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.drawer; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; + +public class ProfileSelectDialog extends DialogFragment implements OnClickListener { + + private static final String ARG_SELECTED_TILE = "selectedTile"; + + private DashboardTile mSelectedTile; + + public static void show(FragmentManager manager, DashboardTile tile) { + ProfileSelectDialog dialog = new ProfileSelectDialog(); + Bundle args = new Bundle(); + args.putParcelable(ARG_SELECTED_TILE, tile); + dialog.setArguments(args); + dialog.show(manager, "select_profile"); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSelectedTile = getArguments().getParcelable(ARG_SELECTED_TILE); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Context context = getActivity(); + AlertDialog.Builder builder = new AlertDialog.Builder(context); + UserAdapter adapter = UserAdapter.createUserAdapter(UserManager.get(context), context, + mSelectedTile.userHandle); + builder.setTitle(com.android.settingslib.R.string.choose_profile) + .setAdapter(adapter, this); + + return builder.create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + UserHandle user = mSelectedTile.userHandle.get(which); + getActivity().startActivityAsUser(mSelectedTile.intent, user); + ((SettingsDrawerActivity) getActivity()).onProfileTileOpen(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java new file mode 100644 index 000000000000..910e6159360c --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java @@ -0,0 +1,144 @@ +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.drawer; + +import android.annotation.LayoutRes; +import android.annotation.Nullable; +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.widget.DrawerLayout; +import android.util.Pair; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.AdapterView; +import android.widget.ListView; +import android.widget.Toolbar; +import com.android.settingslib.R; + +import java.util.HashMap; +import java.util.List; + +public class SettingsDrawerActivity extends Activity { + + private SettingsDrawerAdapter mDrawerAdapter; + // Hold on to a cache of tiles to avoid loading the info multiple times. + private final HashMap<Pair<String, String>, DashboardTile> mTileCache = new HashMap<>(); + private List<DashboardCategory> mDashboardCategories; + private DrawerLayout mDrawerLayout; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + super.setContentView(R.layout.settings_with_drawer); + mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + // Nope. + Toolbar toolbar = (Toolbar) findViewById(R.id.action_bar); + setActionBar(toolbar); + mDrawerAdapter = new SettingsDrawerAdapter(this); + ListView listView = (ListView) findViewById(R.id.left_drawer); + listView.setAdapter(mDrawerAdapter); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + public void onItemClick(android.widget.AdapterView<?> parent, View view, int position, + long id) { + onTileClicked(mDrawerAdapter.getTile(position)); + }; + }); + getActionBar().setHomeAsUpIndicator(R.drawable.ic_menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + openDrawer(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onResume() { + super.onResume(); + + mDrawerAdapter.updateCategories(); + } + + public void openDrawer() { + mDrawerLayout.openDrawer(Gravity.START); + } + + public void closeDrawer() { + mDrawerLayout.closeDrawers(); + } + + @Override + public void setContentView(@LayoutRes int layoutResID) { + LayoutInflater.from(this).inflate(layoutResID, + (ViewGroup) findViewById(R.id.content_frame)); + } + + @Override + public void setContentView(View view) { + ((ViewGroup) findViewById(R.id.content_frame)).addView(view); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + ((ViewGroup) findViewById(R.id.content_frame)).addView(view, params); + } + + public void updateDrawer() { + // TODO: Do this in the background with some loading. + mDrawerAdapter.updateCategories(); + getActionBar().setDisplayHomeAsUpEnabled(mDrawerAdapter.getCount() != 0); + } + + public List<DashboardCategory> getDashboardCategories(boolean force) { + if (force) { + mDashboardCategories = TileUtils.getCategories(this, mTileCache); + } + return mDashboardCategories; + } + + public boolean openTile(DashboardTile tile) { + closeDrawer(); + int numUserHandles = tile.userHandle.size(); + if (numUserHandles > 1) { + ProfileSelectDialog.show(getFragmentManager(), tile); + return false; + } else if (numUserHandles == 1) { + startActivityAsUser(tile.intent, tile.userHandle.get(0)); + } else { + startActivity(tile.intent); + } + return true; + } + + protected void onTileClicked(DashboardTile tile) { + if (openTile(tile)) { + finish(); + } + } + + public void onProfileTileOpen() { + finish(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java new file mode 100644 index 000000000000..fef716c44c57 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerAdapter.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.drawer; + +import android.graphics.drawable.Icon; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.settingslib.R; + +import java.util.ArrayList; +import java.util.List; + +public class SettingsDrawerAdapter extends BaseAdapter { + + private final ArrayList<Item> mItems = new ArrayList<>(); + private final SettingsDrawerActivity mActivity; + + public SettingsDrawerAdapter(SettingsDrawerActivity activity) { + mActivity = activity; + } + + void updateCategories() { + List<DashboardCategory> categories = mActivity.getDashboardCategories(true); + mItems.clear(); + for (int i = 0; i < categories.size(); i++) { + Item category = new Item(); + category.icon = null; + DashboardCategory dashboardCategory = categories.get(i); + category.label = dashboardCategory.title; + mItems.add(category); + for (int j = 0; j < dashboardCategory.tiles.size(); j++) { + Item tile = new Item(); + DashboardTile dashboardTile = dashboardCategory.tiles.get(j); + tile.label = dashboardTile.title; + tile.icon = dashboardTile.icon; + tile.tile = dashboardTile; + mItems.add(tile); + } + } + notifyDataSetChanged(); + } + + public DashboardTile getTile(int position) { + return mItems.get(position).tile; + } + + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mItems.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean isEnabled(int position) { + return mItems.get(position).icon != null; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Item item = mItems.get(position); + boolean isTile = item.icon != null; + if (convertView == null || (isTile != (convertView.getId() == R.id.tile_item))) { + convertView = LayoutInflater.from(mActivity).inflate(isTile ? R.layout.drawer_item + : R.layout.drawer_category, + parent, false); + } + if (isTile) { + ((ImageView) convertView.findViewById(android.R.id.icon)).setImageIcon(item.icon); + } + ((TextView) convertView.findViewById(android.R.id.title)).setText(item.label); + return convertView; + } + + private static class Item { + public Icon icon; + public CharSequence label; + public DashboardTile tile; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java new file mode 100644 index 000000000000..18e8d31f59d3 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.android.settingslib.drawer; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TileUtils { + + private static final boolean DEBUG = false; + + private static final String LOG_TAG = "TileUtils"; + + /** + * Settings will search for system activities of this action and add them as a top level + * settings tile using the following parameters. + * + * <p>A category must be specified in the meta-data for the activity named + * {@link #EXTRA_CATEGORY_KEY} + * + * <p>The title may be defined by meta-data named {@link #META_DATA_PREFERENCE_TITLE} + * otherwise the label for the activity will be used. + * + * <p>The icon may be defined by meta-data named {@link #META_DATA_PREFERENCE_ICON} + * otherwise the icon for the activity will be used. + * + * <p>A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY} + */ + private static final String EXTRA_SETTINGS_ACTION = + "com.android.settings.action.EXTRA_SETTINGS"; + + /** + * Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities. + */ + private static final String SETTINGS_ACTION = + "com.android.settings.action.SETTINGS"; + + private static final String OPERATOR_SETTINGS = + "com.android.settings.OPERATOR_APPLICATION_SETTING"; + + private static final String OPERATOR_DEFAULT_CATEGORY = + "com.android.settings.category.wireless"; + + private static final String MANUFACTURER_SETTINGS = + "com.android.settings.MANUFACTURER_APPLICATION_SETTING"; + + private static final String MANUFACTURER_DEFAULT_CATEGORY = + "com.android.settings.category.device"; + + /** + * The key used to get the category from metadata of activities of action + * {@link #EXTRA_SETTINGS_ACTION} + * The value must be one of: + * <li>com.android.settings.category.wireless</li> + * <li>com.android.settings.category.device</li> + * <li>com.android.settings.category.personal</li> + * <li>com.android.settings.category.system</li> + */ + private static final String EXTRA_CATEGORY_KEY = "com.android.settings.category"; + + /** + * Name of the meta-data item that should be set in the AndroidManifest.xml + * to specify the icon that should be displayed for the preference. + */ + public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon"; + + /** + * Name of the meta-data item that should be set in the AndroidManifest.xml + * to specify the title that should be displayed for the preference. + */ + public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title"; + + /** + * Name of the meta-data item that should be set in the AndroidManifest.xml + * to specify the summary text that should be displayed for the preference. + */ + public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary"; + + private static final String SETTING_PKG = "com.android.settings"; + + public static List<DashboardCategory> getCategories(Context context) { + return getCategories(context, new HashMap<Pair<String, String>, DashboardTile>()); + } + + public static List<DashboardCategory> getCategories(Context context, + HashMap<Pair<String, String>, DashboardTile> cache) { + ArrayList<DashboardTile> tiles = new ArrayList<>(); + UserManager userManager = UserManager.get(context); + for (UserHandle user : userManager.getUserProfiles()) { + // TODO: Needs much optimization, too many PM queries going on here. + if (user.getIdentifier() == ActivityManager.getCurrentUser()) { + // Only add Settings for this user. + getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true); + getTilesForAction(context, user, OPERATOR_SETTINGS, cache, + OPERATOR_DEFAULT_CATEGORY, tiles, false); + getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache, + MANUFACTURER_DEFAULT_CATEGORY, tiles, false); + } + getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false); + } + HashMap<String, DashboardCategory> categoryMap = new HashMap<>(); + for (DashboardTile tile : tiles) { + DashboardCategory category = categoryMap.get(tile.category); + if (category == null) { + category = createCategory(context, tile.category); + if (category == null) { + Log.w(LOG_TAG, "Couldn't find category " + tile.category); + continue; + } + categoryMap.put(category.key, category); + } + category.addTile(tile); + } + ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values()); + for (DashboardCategory category : categories) { + Collections.sort(category.tiles, TILE_COMPARATOR); + } + Collections.sort(categories, CATEGORY_COMPARATOR); + return categories; + } + + private static DashboardCategory createCategory(Context context, String categoryKey) { + DashboardCategory category = new DashboardCategory(); + category.key = categoryKey; + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> results = pm.queryIntentActivities(new Intent(categoryKey), 0); + if (results.size() == 0) { + return null; + } + for (ResolveInfo resolved : results) { + if (!resolved.system) { + // Do not allow any app to add to settings, only system ones. + continue; + } + category.title = resolved.activityInfo.loadLabel(pm); + category.priority = SETTING_PKG.equals( + resolved.activityInfo.applicationInfo.packageName) ? resolved.priority : 0; + if (DEBUG) Log.d(LOG_TAG, "Adding category " + category.title); + } + + return category; + } + + private static void getTilesForAction(Context context, + UserHandle user, String action, Map<Pair<String, String>, DashboardTile> addedCache, + String defaultCategory, ArrayList<DashboardTile> outTiles, boolean requireSettings) { + PackageManager pm = context.getPackageManager(); + Intent intent = new Intent(action); + List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent, + PackageManager.GET_META_DATA, user.getIdentifier()); + for (ResolveInfo resolved : results) { + if (requireSettings) { + if (!SETTING_PKG.equals(resolved.activityInfo.applicationInfo.packageName)) { + continue; + } + } else { + if (!resolved.system) { + // Do not allow any app to add to settings, only system ones. + continue; + } + } + ActivityInfo activityInfo = resolved.activityInfo; + Bundle metaData = activityInfo.metaData; + String categoryKey = defaultCategory; + if (((metaData == null) || !metaData.containsKey(EXTRA_CATEGORY_KEY)) + && categoryKey == null) { + Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for action " + + action + " missing metadata " + + (metaData == null ? "" : EXTRA_CATEGORY_KEY)); + continue; + } else { + categoryKey = metaData.getString(EXTRA_CATEGORY_KEY); + } + Pair<String, String> key = new Pair<String, String>(activityInfo.packageName, + activityInfo.name); + DashboardTile tile = addedCache.get(key); + if (tile == null) { + tile = new DashboardTile(); + tile.intent = new Intent().setClassName( + activityInfo.packageName, activityInfo.name); + tile.category = categoryKey; + tile.priority = requireSettings ? resolved.priority : 0; + updateTileData(context, tile); + if (DEBUG) Log.d(LOG_TAG, "Adding tile " + tile.title); + + addedCache.put(key, tile); + } + if (!tile.userHandle.contains(user)) { + tile.userHandle.add(user); + } + if (!outTiles.contains(tile)) { + outTiles.add(tile); + } + } + } + + private static DashboardCategory getCategory(List<DashboardCategory> target, + String categoryKey) { + for (DashboardCategory category : target) { + if (categoryKey.equals(category.key)) { + return category; + } + } + return null; + } + + private static boolean updateTileData(Context context, DashboardTile tile) { + Intent intent = tile.intent; + if (intent != null) { + // Find the activity that is in the system image + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> list = tile.userHandle.size() != 0 + ? pm.queryIntentActivitiesAsUser(intent, PackageManager.GET_META_DATA, + tile.userHandle.get(0).getIdentifier()) + : pm.queryIntentActivities(intent, PackageManager.GET_META_DATA); + int listSize = list.size(); + for (int i = 0; i < listSize; i++) { + ResolveInfo resolveInfo = list.get(i); + if (resolveInfo.activityInfo.applicationInfo.isSystemApp()) { + int icon = 0; + CharSequence title = null; + String summary = null; + + // Get the activity's meta-data + try { + Resources res = pm.getResourcesForApplication( + resolveInfo.activityInfo.packageName); + Bundle metaData = resolveInfo.activityInfo.metaData; + + if (res != null && metaData != null) { + if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) { + icon = metaData.getInt(META_DATA_PREFERENCE_ICON); + } + if (metaData.containsKey(META_DATA_PREFERENCE_TITLE)) { + title = metaData.getString(META_DATA_PREFERENCE_TITLE); + } + if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) { + summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY); + } + } + } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { + if (DEBUG) Log.d(LOG_TAG, "Couldn't find info", e); + } + + // Set the preference title to the activity's label if no + // meta-data is found + if (TextUtils.isEmpty(title)) { + title = resolveInfo.loadLabel(pm).toString(); + } + if (icon == 0) { + icon = resolveInfo.activityInfo.icon; + } + + // Set icon, title and summary for the preference + tile.icon = Icon.createWithResource(resolveInfo.activityInfo.packageName, icon); + tile.title = title; + tile.summary = summary; + // Replace the intent with this specific activity + tile.intent = new Intent().setClassName(resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name); + + return true; + } + } + } + + return false; + } + + private static final Comparator<DashboardTile> TILE_COMPARATOR = + new Comparator<DashboardTile>() { + @Override + public int compare(DashboardTile lhs, DashboardTile rhs) { + return rhs.priority - lhs.priority; + } + }; + + private static final Comparator<DashboardCategory> CATEGORY_COMPARATOR = + new Comparator<DashboardCategory>() { + @Override + public int compare(DashboardCategory lhs, DashboardCategory rhs) { + return rhs.priority - lhs.priority; + } + }; +} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java b/packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java new file mode 100644 index 000000000000..f9fa805f808d --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.drawer; + +import android.app.ActivityManager; + +import android.content.Context; +import android.content.pm.UserInfo; +import android.database.DataSetObserver; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.os.UserManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.SpinnerAdapter; +import android.widget.TextView; +import com.android.internal.util.UserIcons; +import com.android.settingslib.drawable.CircleFramedDrawable; + +import com.android.settingslib.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adapter for a spinner that shows a list of users. + */ +public class UserAdapter implements SpinnerAdapter, ListAdapter { + /** Holder for user details */ + public static class UserDetails { + private final UserHandle mUserHandle; + private final String mName; + private final Drawable mIcon; + + public UserDetails(UserHandle userHandle, UserManager um, Context context) { + mUserHandle = userHandle; + UserInfo userInfo = um.getUserInfo(mUserHandle.getIdentifier()); + Drawable icon; + if (userInfo.isManagedProfile()) { + mName = context.getString(R.string.managed_user_title); + icon = context.getDrawable( + com.android.internal.R.drawable.ic_corp_icon); + } else { + mName = userInfo.name; + final int userId = userInfo.id; + if (um.getUserIcon(userId) != null) { + icon = new BitmapDrawable(context.getResources(), um.getUserIcon(userId)); + } else { + icon = UserIcons.getDefaultUserIcon(userId, /* light= */ false); + } + } + this.mIcon = encircle(context, icon); + } + + private static Drawable encircle(Context context, Drawable icon) { + return CircleFramedDrawable.getInstance(context, UserIcons.convertToBitmap(icon)); + } + } + private ArrayList<UserDetails> data; + private final LayoutInflater mInflater; + + public UserAdapter(Context context, ArrayList<UserDetails> users) { + if (users == null) { + throw new IllegalArgumentException("A list of user details must be provided"); + } + this.data = users; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public UserHandle getUserHandle(int position) { + if (position < 0 || position >= data.size()) { + return null; + } + return data.get(position).mUserHandle; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + final View row = convertView != null ? convertView : createUser(parent); + + UserDetails user = data.get(position); + ((ImageView) row.findViewById(android.R.id.icon)).setImageDrawable(user.mIcon); + ((TextView) row.findViewById(android.R.id.title)).setText(getTitle(user)); + return row; + } + + private int getTitle(UserDetails user) { + int userHandle = user.mUserHandle.getIdentifier(); + if (userHandle == UserHandle.USER_CURRENT + || userHandle == ActivityManager.getCurrentUser()) { + return R.string.category_personal; + } else { + return R.string.category_work; + } + } + + private View createUser(ViewGroup parent) { + return mInflater.inflate(R.layout.user_preference, parent, false); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + // We don't support observers + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + // We don't support observers + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public UserDetails getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return data.get(position).mUserHandle.getIdentifier(); + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return getDropDownView(position, convertView, parent); + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + /** + * Creates a {@link UserAdapter} if there is more than one profile on the device. + * + * <p> The adapter can be used to populate a spinner that switches between the Settings + * app on the different profiles. + * + * @return a {@link UserAdapter} or null if there is only one profile. + */ + public static UserAdapter createUserSpinnerAdapter(UserManager userManager, + Context context) { + List<UserHandle> userProfiles = userManager.getUserProfiles(); + if (userProfiles.size() < 2) { + return null; + } + + UserHandle myUserHandle = new UserHandle(UserHandle.myUserId()); + // The first option should be the current profile + userProfiles.remove(myUserHandle); + userProfiles.add(0, myUserHandle); + + return createUserAdapter(userManager, context, userProfiles); + } + + public static UserAdapter createUserAdapter(UserManager userManager, + Context context, List<UserHandle> userProfiles) { + ArrayList<UserDetails> userDetails = new ArrayList<UserDetails>(userProfiles.size()); + final int count = userProfiles.size(); + for (int i = 0; i < count; i++) { + userDetails.add(new UserDetails(userProfiles.get(i), userManager, context)); + } + return new UserAdapter(context, userDetails); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java index b2dd5d97e801..ab05bb08a0ff 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java @@ -283,8 +283,6 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView * Dismisses recents if we are already visible and the intent is to toggle the recents view. */ boolean dismissRecentsToFocusedTaskOrHome(boolean checkFilteredStackState) { - RecentsConfiguration config = Recents.getConfiguration(); - RecentsActivityLaunchState launchState = config.getLaunchState(); SystemServicesProxy ssp = Recents.getSystemServices(); if (ssp.isRecentsTopMost(ssp.getTopMostTask(), null)) { // If we currently have filtered stacks, then unfilter those first @@ -292,13 +290,6 @@ public class RecentsActivity extends Activity implements RecentsView.RecentsView mRecentsView.unfilterFilteredStacks()) return true; // If we have a focused Task, launch that Task now if (mRecentsView.launchFocusedTask()) return true; - // If we launched from Home, then return to Home - if (launchState.launchedFromHome) { - dismissRecentsToHome(true); - return true; - } - // Otherwise, try and return to the Task that Recents was launched from - if (mRecentsView.launchPreviousTask()) return true; // If none of the other cases apply, then just go Home dismissRecentsToHome(true); return true; diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java index 31ce569b9abf..9ee6f5b4d8e2 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivityLaunchState.java @@ -70,4 +70,41 @@ public class RecentsActivityLaunchState { RecentsConfiguration config = Recents.getConfiguration(); return !launchedWithNoRecentTasks && !config.hasTransposedNavBar; } + + /** + * Returns the task to focus given the current launch state. + */ + public int getInitialFocusTaskIndex(int numTasks) { + if (Constants.DebugFlags.App.EnableFastToggleRecents && !launchedWithAltTab) { + // If we are fast toggling, then focus the next task depending on when you are on home + // or coming in from another app + if (launchedFromHome) { + return numTasks - 1; + } else { + return numTasks - 2; + } + } + + if (launchedWithAltTab && launchedFromAppWithThumbnail) { + // If alt-tabbing from another app, focus the next task + return numTasks - 2; + } else if ((launchedWithAltTab && launchedFromHome) || + (!launchedWithAltTab && launchedFromAppWithThumbnail)) { + // If alt-tabbing from home, or launching from an app normally, focus that task + return numTasks - 1; + } else { + // Otherwise, we are launching recents from home normally, focus no tasks so that we + // know to return home + return -1; + } + } + + @Override + public String toString() { + return "RecentsActivityLaunchState altTab: " + launchedWithAltTab + + ", noTasks: " + launchedWithNoRecentTasks + + ", fromHome: " + launchedFromHome + + ", fromSearchHome: " + launchedFromSearchHome + + ", reuse: " + launchedReuseTaskStackViews; + } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java index e2f20fdefbb3..a73f3234ed44 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java @@ -110,7 +110,8 @@ public class RecentsConfiguration { void update(Context context, SystemServicesProxy ssp, Rect windowRect) { // Only update resources that can change after the first load, either through developer // settings or via multi window - lockToAppEnabled = ssp.getSystemSetting(context, Settings.System.LOCK_TO_APP_ENABLED) != 0; + lockToAppEnabled = !ssp.hasFreeformWorkspaceSupport() && + ssp.getSystemSetting(context, Settings.System.LOCK_TO_APP_ENABLED) != 0; hasDockedTasks = ssp.hasDockedTask(); // Recompute some values based on the given state, since we can not rely on the resource diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java index 0e11f02a34ec..675ef2fe55ab 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java @@ -272,7 +272,10 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask(); MutableBoolean isTopTaskHome = new MutableBoolean(true); if (topTask != null && ssp.isRecentsTopMost(topTask, isTopTaskHome)) { - if (Constants.DebugFlags.App.EnableFastToggleRecents) { + RecentsConfiguration config = Recents.getConfiguration(); + RecentsActivityLaunchState launchState = config.getLaunchState(); + if (Constants.DebugFlags.App.EnableFastToggleRecents && + !launchState.launchedWithAltTab) { // Notify recents to move onto the next task EventBus.getDefault().post(new IterateRecentsEvent()); } else { @@ -511,7 +514,7 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub reloadHeaderBarLayout(false /* tryAndBindSearchWidget */); // Update the destination rect - mDummyStackView.updateMinMaxScrollForStack(stack); + mDummyStackView.updateLayoutForStack(stack); final Task toTask = new Task(); final TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView, topTask.id, toTask); @@ -677,7 +680,6 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub */ private void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, boolean isTopTaskHome) { - SystemServicesProxy ssp = Recents.getSystemServices(); RecentsTaskLoader loader = Recents.getTaskLoader(); // Update the header bar if necessary @@ -694,7 +696,7 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub TaskStack stack = sInstanceLoadPlan.getTaskStack(); // Prepare the dummy stack for the transition - mDummyStackView.updateMinMaxScrollForStack(stack); + mDummyStackView.updateLayoutForStack(stack); TaskStackLayoutAlgorithm.VisibilityReport stackVr = mDummyStackView.computeStackVisibilityReport(); boolean hasRecentTasks = stack.getTaskCount() > 0; @@ -717,6 +719,7 @@ public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub // If there is no thumbnail transition, but is launching from home into recents, then // use a quick home transition and do the animation from home if (!Constants.DebugFlags.App.DisableSearchBar && hasRecentTasks) { + SystemServicesProxy ssp = Recents.getSystemServices(); String homeActivityPackage = ssp.getHomeActivityPackageName(); String searchWidgetPackage = Prefs.getString(mContext, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, null); diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/EventBus.java b/packages/SystemUI/src/com/android/systemui/recents/events/EventBus.java index deae4c8d5ff9..b091f050a340 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/events/EventBus.java +++ b/packages/SystemUI/src/com/android/systemui/recents/events/EventBus.java @@ -549,7 +549,7 @@ public class EventBus extends BroadcastReceiver { InvocationTargetException| InstantiationException| IllegalAccessException e) { - Log.e(TAG, "Failed to create InterprocessEvent", e); + Log.e(TAG, "Failed to create InterprocessEvent", e.getCause()); } } @@ -746,9 +746,9 @@ public class EventBus extends BroadcastReceiver { Log.e(TAG, "Failed to deliver event to null subscriber"); } } catch (IllegalAccessException e) { - Log.e(TAG, "Failed to invoke method", e); + Log.e(TAG, "Failed to invoke method", e.getCause()); } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); + Log.e(TAG, "Failed to invoke method", e.getCause()); } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragDockStateChangedEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragDropTargetChangedEvent.java index f2c3c338383e..21321f297159 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragDockStateChangedEvent.java +++ b/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragDropTargetChangedEvent.java @@ -18,18 +18,18 @@ package com.android.systemui.recents.events.ui.dragndrop; import com.android.systemui.recents.events.EventBus; import com.android.systemui.recents.model.Task; -import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.recents.views.DropTarget; /** - * This event is sent when a user drag enters or exits a dock region. + * This event is sent when a user drags in/out of a drop target. */ -public class DragDockStateChangedEvent extends EventBus.Event { +public class DragDropTargetChangedEvent extends EventBus.Event { public final Task task; - public final TaskStack.DockState dockState; + public final DropTarget dropTarget; - public DragDockStateChangedEvent(Task task, TaskStack.DockState dockState) { + public DragDropTargetChangedEvent(Task task, DropTarget dropTarget) { this.task = task; - this.dockState = dockState; + this.dropTarget = dropTarget; } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragEndEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragEndEvent.java index 827998d8529f..957da9441ec6 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragEndEvent.java +++ b/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragEndEvent.java @@ -19,8 +19,8 @@ package com.android.systemui.recents.events.ui.dragndrop; import com.android.systemui.recents.events.EventBus; import com.android.systemui.recents.misc.ReferenceCountedTrigger; import com.android.systemui.recents.model.Task; -import com.android.systemui.recents.model.TaskStack; import com.android.systemui.recents.views.DragView; +import com.android.systemui.recents.views.DropTarget; import com.android.systemui.recents.views.TaskView; /** @@ -31,15 +31,15 @@ public class DragEndEvent extends EventBus.Event { public final Task task; public final TaskView taskView; public final DragView dragView; - public final TaskStack.DockState dockState; + public final DropTarget dropTarget; public final ReferenceCountedTrigger postAnimationTrigger; - public DragEndEvent(Task task, TaskView taskView, DragView dragView, - TaskStack.DockState dockState, ReferenceCountedTrigger postAnimationTrigger) { + public DragEndEvent(Task task, TaskView taskView, DragView dragView, DropTarget dropTarget, + ReferenceCountedTrigger postAnimationTrigger) { this.task = task; this.taskView = taskView; this.dragView = dragView; - this.dockState = dockState; + this.dropTarget = dropTarget; this.postAnimationTrigger = postAnimationTrigger; } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragStartInitializeDropTargetsEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragStartInitializeDropTargetsEvent.java new file mode 100644 index 000000000000..b450b1fdcaec --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/events/ui/dragndrop/DragStartInitializeDropTargetsEvent.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.events.ui.dragndrop; + +import com.android.systemui.recents.events.EventBus; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.views.RecentsViewTouchHandler; + +/** + * This event is sent by the drag manager when it requires drop targets to register themselves for + * the current drag gesture. + */ +public class DragStartInitializeDropTargetsEvent extends EventBus.Event { + + public final Task task; + public final RecentsViewTouchHandler handler; + + public DragStartInitializeDropTargetsEvent(Task task, RecentsViewTouchHandler handler) { + this.task = task; + this.handler = handler; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java index d99886d951ad..4ee0357123aa 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java +++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java @@ -103,6 +103,7 @@ public class SystemServicesProxy { Display mDisplay; String mRecentsPackage; ComponentName mAssistComponent; + boolean mHasFreeformWorkspaceSupport; Bitmap mDummyIcon; int mDummyThumbnailWidth; @@ -123,6 +124,8 @@ public class SystemServicesProxy { mUm = UserManager.get(context); mDisplay = mWm.getDefaultDisplay(); mRecentsPackage = context.getPackageName(); + mHasFreeformWorkspaceSupport = false && mPm.hasSystemFeature( + PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT); // Get the dummy thumbnail width/heights Resources res = context.getResources(); @@ -241,7 +244,7 @@ public class SystemServicesProxy { public boolean hasFreeformWorkspaceSupport() { if (mPm == null) return false; - return mPm.hasSystemFeature(PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT); + return mHasFreeformWorkspaceSupport; } /** Returns whether the recents is currently running */ @@ -411,6 +414,19 @@ public class SystemServicesProxy { return thumbnail; } + /** + * Moves a task into another stack. + */ + public void moveTaskToStack(int taskId, int stackId) { + if (mIam == null) return; + + try { + mIam.positionTaskInStack(taskId, stackId, 0); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + /** Moves a task to the front with the specified activity options. */ public void moveTaskToFront(int taskId, ActivityOptions opts) { if (mAm == null) return; diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java index 51a1ebc579e4..12bd556b8655 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java @@ -22,6 +22,8 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import com.android.systemui.recents.Recents; +import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.Utilities; import java.util.Objects; @@ -165,7 +167,9 @@ public class Task { this.group = group; } - /** Updates the stack id of this task. */ + /** + * Updates the stack id of this task. + */ public void setStackId(int stackId) { key.stackId = stackId; if (mCb != null) { @@ -173,10 +177,12 @@ public class Task { } } + /** + * Returns whether this task is on the freeform task stack. + */ public boolean isFreeformTask() { - // Temporarily disable: - return false; - // return SystemServicesProxy.isFreeformStack(key.stackId); + SystemServicesProxy ssp = Recents.getSystemServices(); + return ssp.hasFreeformWorkspaceSupport() && ssp.isFreeformStack(key.stackId); } /** Notifies the callback listeners that this task has been loaded */ @@ -210,7 +216,7 @@ public class Task { if (group != null) { groupAffiliation = Integer.toString(group.affiliation); } - return "Task (" + groupAffiliation + "): " + key.getComponent().getPackageName() + + return "Task (" + groupAffiliation + "): " + key + " [" + super.toString() + "]"; } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java index a96fe98bd817..495c8fd2bc5d 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java @@ -17,18 +17,21 @@ package com.android.systemui.recents.model; import android.animation.ObjectAnimator; +import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.graphics.Color; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; +import android.util.Log; import com.android.systemui.R; import com.android.systemui.recents.Constants; import com.android.systemui.recents.Recents; import com.android.systemui.recents.misc.NamedCounter; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.Utilities; +import com.android.systemui.recents.views.DropTarget; import java.util.ArrayList; import java.util.Collections; @@ -40,6 +43,8 @@ import java.util.Random; import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT; import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT; +import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; +import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; /** @@ -54,9 +59,13 @@ interface TaskFilter { * A list of filtered tasks. */ class FilteredTaskList { - ArrayList<Task> mTasks = new ArrayList<Task>(); - ArrayList<Task> mFilteredTasks = new ArrayList<Task>(); - HashMap<Task.TaskKey, Integer> mTaskIndices = new HashMap<Task.TaskKey, Integer>(); + + private static final String TAG = "FilteredTaskList"; + private static final boolean DEBUG = true; + + ArrayList<Task> mTasks = new ArrayList<>(); + ArrayList<Task> mFilteredTasks = new ArrayList<>(); + HashMap<Task.TaskKey, Integer> mTaskIndices = new HashMap<>(); TaskFilter mFilter; /** Sets the task filter, saving the current touch state */ @@ -93,6 +102,25 @@ class FilteredTaskList { updateFilteredTasks(); } + /** + * Moves the given task. + */ + public void moveTaskToStack(Task task, int insertIndex, int newStackId) { + int taskIndex = indexOf(task); + if (taskIndex != insertIndex) { + mTasks.remove(taskIndex); + if (taskIndex < insertIndex) { + insertIndex--; + } + mTasks.add(insertIndex, task); + } + + // Update the stack id now, after we've moved the task, and before we update the + // filtered tasks + task.setStackId(newStackId); + updateFilteredTasks(); + } + /** Sets the list of tasks */ void set(List<Task> tasks) { mTasks.clear(); @@ -187,16 +215,21 @@ public class TaskStack { } - public enum DockState { - NONE(-1, 96, null, null, null), + public enum DockState implements DropTarget { + NONE(-1, 96, null, null), LEFT(DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, 192, - new RectF(0, 0, 0.3f, 1), new RectF(0, 0, 0.3f, 1), new RectF(0.7f, 0, 1, 1)), + new RectF(0, 0, 0.25f, 1), new RectF(0, 0, 0.25f, 1)), TOP(DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, 192, - new RectF(0, 0, 1, 0.3f), new RectF(0, 0, 1, 0.3f), new RectF(0, 0.7f, 1, 1)), + new RectF(0, 0, 1, 0.25f), new RectF(0, 0, 1, 0.25f)), RIGHT(DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT, 192, - new RectF(0.7f, 0, 1, 1), new RectF(0.7f, 0, 1, 1), new RectF(0, 0, 0.3f, 1)), + new RectF(0.75f, 0, 1, 1), new RectF(0.75f, 0, 1, 1)), BOTTOM(DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT, 192, - new RectF(0, 0.7f, 1, 1), new RectF(0, 0.7f, 1, 1), new RectF(0, 0, 1, 0.3f)); + new RectF(0, 0.75f, 1, 1), new RectF(0, 0.75f, 1, 1)); + + @Override + public boolean acceptsDrop(int x, int y, int width, int height) { + return touchAreaContainsPoint(width, height, x, y); + } // Represents the view state of this dock state public class ViewState { @@ -229,20 +262,17 @@ public class TaskStack { public final ViewState viewState; private final RectF dockArea; private final RectF touchArea; - private final RectF stackArea; /** * @param createMode used to pass to ActivityManager to dock the task * @param touchArea the area in which touch will initiate this dock state - * @param stackArea the area for the stack if a task is docked + * @param dockArea the visible dock area */ - DockState(int createMode, int dockAreaAlpha, RectF touchArea, RectF dockArea, - RectF stackArea) { + DockState(int createMode, int dockAreaAlpha, RectF touchArea, RectF dockArea) { this.createMode = createMode; this.viewState = new ViewState(dockAreaAlpha); this.dockArea = dockArea; this.touchArea = touchArea; - this.stackArea = stackArea; } /** @@ -264,18 +294,6 @@ public class TaskStack { return new Rect((int) (dockArea.left * width), (int) (dockArea.top * height), (int) (dockArea.right * width), (int) (dockArea.bottom * height)); } - - /** - * Returns the stack bounds with the given {@param width} and {@param height}. - */ - public Rect getStackBounds(Rect stackRect) { - int width = stackRect.width(); - int height = stackRect.height(); - return new Rect((int) (stackRect.left + stackArea.left * width), - (int) (stackRect.top + stackArea.top * height), - (int) (stackRect.left + (stackArea.right - stackArea.left) * width), - (int) (stackRect.top + (stackArea.bottom - stackArea.top) * height)); - } } // The task offset to apply to a task id as a group affiliation @@ -308,6 +326,29 @@ public class TaskStack { } } + /** + * Moves the given task to either the front of the freeform workspace or the stack. + */ + public void moveTaskToStack(Task task, int newStackId) { + // Find the index to insert into + ArrayList<Task> taskList = mTaskList.getTasks(); + int taskCount = taskList.size(); + if (!task.isFreeformTask() && (newStackId == FREEFORM_WORKSPACE_STACK_ID)) { + // Insert freeform tasks at the front + mTaskList.moveTaskToStack(task, taskCount, newStackId); + } else if (task.isFreeformTask() && (newStackId == FULLSCREEN_WORKSPACE_STACK_ID)) { + // Insert after the first stacked task + int insertIndex = 0; + for (int i = taskCount - 1; i >= 0; i--) { + if (!taskList.get(i).isFreeformTask()) { + insertIndex = i + 1; + break; + } + } + mTaskList.moveTaskToStack(task, insertIndex, newStackId); + } + } + /** Does the actual work associated with removing the task. */ void removeTaskImpl(Task t) { // Remove the task from the list diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/DropTarget.java b/packages/SystemUI/src/com/android/systemui/recents/views/DropTarget.java new file mode 100644 index 000000000000..8ae00a7eae82 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/DropTarget.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +/** + * Represents a drop target for a drag gesture. + */ +public interface DropTarget { + + /** + * Returns whether this target can accept this drop. The x,y are relative to the top level + * RecentsView, and the width/height are of the RecentsView. + */ + boolean acceptsDrop(int x, int y, int width, int height); +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java index a97a2a800ac4..765f6861d7a1 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java @@ -111,6 +111,11 @@ public class FreeformWorkspaceLayoutAlgorithm { Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale); transformOut.visible = true; transformOut.p = 0; + + if (DEBUG) { + Log.d(TAG, "getTransform: " + task.key + ", " + transformOut); + } + return transformOut; } return null; diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java index 190abe4d369f..1f80460714ba 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -16,7 +16,6 @@ package com.android.systemui.recents.views; -import android.app.ActivityManager; import android.app.ActivityOptions; import android.content.Context; import android.graphics.Bitmap; @@ -51,7 +50,7 @@ import com.android.systemui.recents.events.EventBus; import com.android.systemui.recents.events.activity.DismissRecentsToHomeAnimationStarted; import com.android.systemui.recents.events.component.ScreenPinningRequestEvent; import com.android.systemui.recents.events.ui.DismissTaskViewEvent; -import com.android.systemui.recents.events.ui.dragndrop.DragDockStateChangedEvent; +import com.android.systemui.recents.events.ui.dragndrop.DragDropTargetChangedEvent; import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent; import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent; import com.android.systemui.recents.misc.SystemServicesProxy; @@ -220,30 +219,6 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV return false; } - /** Launches the task that Recents was launched from, if possible */ - public boolean launchPreviousTask() { - if (mTaskStackView != null) { - TaskStack stack = mTaskStackView.getStack(); - ArrayList<Task> tasks = stack.getTasks(); - - // Find the launch task in the stack - // TODO: replace this with an event from RecentsActivity - if (!tasks.isEmpty()) { - int taskCount = tasks.size(); - for (int j = 0; j < taskCount; j++) { - if (tasks.get(j).isLaunchTarget) { - Task task = tasks.get(j); - TaskView tv = mTaskStackView.getChildViewForTask(task); - onTaskViewClicked(mTaskStackView, tv, stack, task, false, false, null, - INVALID_STACK_ID); - return true; - } - } - } - } - return false; - } - /** Requests all task stacks to start their enter-recents animation */ public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) { // We have to increment/decrement the post animation trigger in case there are no children @@ -338,9 +313,10 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV } if (mDragView != null) { + Rect taskRect = mTaskStackView.mLayoutAlgorithm.mTaskRect; mDragView.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); + MeasureSpec.makeMeasureSpec(taskRect.width(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(taskRect.height(), MeasureSpec.AT_MOST)); } setMeasuredDimension(width, height); @@ -789,52 +765,76 @@ public class RecentsView extends FrameLayout implements TaskStackView.TaskStackV TaskStack.DockState.NONE.viewState.dockAreaAlpha); } - public final void onBusEvent(DragDockStateChangedEvent event) { - if (event.dockState == TaskStack.DockState.NONE) { + public final void onBusEvent(DragDropTargetChangedEvent event) { + if (event.dropTarget == null || !(event.dropTarget instanceof TaskStack.DockState)) { updateVisibleDockRegions(mTouchHandler.getDockStatesForCurrentOrientation(), TaskStack.DockState.NONE.viewState.dockAreaAlpha); } else { - updateVisibleDockRegions(new TaskStack.DockState[] {event.dockState}, -1); + final TaskStack.DockState dockState = (TaskStack.DockState) event.dropTarget; + updateVisibleDockRegions(new TaskStack.DockState[] {dockState}, -1); } } public final void onBusEvent(final DragEndEvent event) { - event.postAnimationTrigger.increment(); - event.postAnimationTrigger.addLastDecrementRunnable(new Runnable() { + final Runnable cleanUpRunnable = new Runnable() { @Override public void run() { // Remove the drag view removeView(mDragView); mDragView = null; - updateVisibleDockRegions(null, -1); + } + }; + + // Animate the overlay alpha back to 0 + updateVisibleDockRegions(null, -1); - // Dock the new task if we are hovering over a valid dock state - if (event.dockState != TaskStack.DockState.NONE) { - SystemServicesProxy ssp = Recents.getSystemServices(); - ssp.startTaskInDockedMode(event.task.key.id, event.dockState.createMode); - launchTask(event.task, null, INVALID_STACK_ID); + if (event.dropTarget == null) { + // No drop targets for hit, so just animate the task back to its place + event.postAnimationTrigger.increment(); + event.postAnimationTrigger.addLastDecrementRunnable(new Runnable() { + @Override + public void run() { + cleanUpRunnable.run(); } - } - }); - if (event.dockState == TaskStack.DockState.NONE) { - // Animate the alpha back to what it was before - Rect taskBounds = mTaskStackView.getStackAlgorithm().getUntransformedTaskViewBounds(); - int left = taskBounds.left + (int) ((1f - event.taskView.getScaleX()) * taskBounds.width()) / 2; - int top = taskBounds.top + (int) ((1f - event.taskView.getScaleY()) * taskBounds.height()) / 2; + }); + // Animate the task back to where it was before then clean up afterwards + TaskViewTransform taskTransform = new TaskViewTransform(); + TaskStackLayoutAlgorithm layoutAlgorithm = mTaskStackView.getStackAlgorithm(); + layoutAlgorithm.getStackTransform(event.task, + mTaskStackView.getScroller().getStackScroll(), taskTransform, null); event.dragView.animate() - .scaleX(1f) - .scaleY(1f) - .translationX(left + event.taskView.getTranslationX()) - .translationY(top + event.taskView.getTranslationY()) + .scaleX(taskTransform.scale) + .scaleY(taskTransform.scale) + .translationX((layoutAlgorithm.mTaskRect.left - event.dragView.getLeft()) + + taskTransform.translationX) + .translationY((layoutAlgorithm.mTaskRect.top - event.dragView.getTop()) + + taskTransform.translationY) .setDuration(175) .setInterpolator(mFastOutSlowInInterpolator) .withEndAction(event.postAnimationTrigger.decrementAsRunnable()) .start(); - // Animate the overlay alpha back to 0 - updateVisibleDockRegions(null, -1); + } else if (event.dropTarget instanceof TaskStack.DockState) { + final TaskStack.DockState dockState = (TaskStack.DockState) event.dropTarget; + + // For now, just remove the drag view and the original task + // TODO: Animate the task to the drop target rect before launching it above + cleanUpRunnable.run(); + + // Dock the task and launch it + SystemServicesProxy ssp = Recents.getSystemServices(); + ssp.startTaskInDockedMode(event.task.key.id, dockState.createMode); + launchTask(event.task, null, INVALID_STACK_ID); + } else { - event.postAnimationTrigger.decrement(); + // We dropped on another drop target, so just add the cleanup to the post animation + // trigger + event.postAnimationTrigger.addLastDecrementRunnable(new Runnable() { + @Override + public void run() { + cleanUpRunnable.run(); + } + }); } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewTouchHandler.java index f3a43905a823..c7edc92fc207 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewTouchHandler.java @@ -22,13 +22,16 @@ import android.view.MotionEvent; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.events.EventBus; -import com.android.systemui.recents.events.ui.dragndrop.DragDockStateChangedEvent; +import com.android.systemui.recents.events.ui.dragndrop.DragDropTargetChangedEvent; import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent; import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent; +import com.android.systemui.recents.events.ui.dragndrop.DragStartInitializeDropTargetsEvent; import com.android.systemui.recents.misc.ReferenceCountedTrigger; import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskStack; +import java.util.ArrayList; + /** * Represents the dock regions for each orientation. @@ -52,7 +55,10 @@ class DockRegion { /** * Handles touch events for a RecentsView. */ -class RecentsViewTouchHandler { +public class RecentsViewTouchHandler { + + private static final String TAG = "RecentsViewTouchHandler"; + private static final boolean DEBUG = false; private RecentsView mRv; @@ -62,13 +68,22 @@ class RecentsViewTouchHandler { private Point mDownPos = new Point(); private boolean mDragging; - private TaskStack.DockState mLastDockState = TaskStack.DockState.NONE; + + private DropTarget mLastDropTarget; + private ArrayList<DropTarget> mDropTargets = new ArrayList<>(); public RecentsViewTouchHandler(RecentsView rv) { mRv = rv; } /** + * Registers a new drop target for the current drag only. + */ + public void registerDropTargetForCurrentDrag(DropTarget target) { + mDropTargets.add(target); + } + + /** * Returns the preferred dock states for the current orientation. */ public TaskStack.DockState[] getDockStatesForCurrentOrientation() { @@ -101,11 +116,24 @@ class RecentsViewTouchHandler { mDragTask = event.task; mTaskView = event.taskView; mDragView = event.dragView; + mDropTargets.clear(); float x = mDownPos.x - mDragView.getTopLeftOffset().x; float y = mDownPos.y - mDragView.getTopLeftOffset().y; mDragView.setTranslationX(x); mDragView.setTranslationY(y); + + RecentsConfiguration config = Recents.getConfiguration(); + if (!config.hasDockedTasks) { + // Add the dock state drop targets (these take priority) + TaskStack.DockState[] dockStates = getDockStatesForCurrentOrientation(); + for (TaskStack.DockState dockState : dockStates) { + registerDropTargetForCurrentDrag(dockState); + } + } + + // Request other drop targets to register themselves + EventBus.getDefault().send(new DragStartInitializeDropTargetsEvent(event.task, this)); } public final void onBusEvent(DragEndEvent event) { @@ -113,7 +141,7 @@ class RecentsViewTouchHandler { mDragTask = null; mTaskView = null; mDragView = null; - mLastDockState = TaskStack.DockState.NONE; + mLastDropTarget = null; } /** @@ -121,8 +149,6 @@ class RecentsViewTouchHandler { * @param ev */ private void handleTouchEvent(MotionEvent ev) { - boolean isLandscape = mRv.getResources().getConfiguration().orientation == - Configuration.ORIENTATION_LANDSCAPE; int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: @@ -137,20 +163,17 @@ class RecentsViewTouchHandler { float x = evX - mDragView.getTopLeftOffset().x; float y = evY - mDragView.getTopLeftOffset().y; - // Update the dock state - TaskStack.DockState[] dockStates = getDockStatesForCurrentOrientation(); - TaskStack.DockState foundDockState = TaskStack.DockState.NONE; - for (int i = dockStates.length - 1; i >= 0; i--) { - TaskStack.DockState state = dockStates[i]; - if (state.touchAreaContainsPoint(width, height, evX, evY)) { - foundDockState = state; + DropTarget currentDropTarget = null; + for (DropTarget target : mDropTargets) { + if (target.acceptsDrop((int) evX, (int) evY, width, height)) { + currentDropTarget = target; break; } } - if (mLastDockState != foundDockState) { - mLastDockState = foundDockState; - EventBus.getDefault().send(new DragDockStateChangedEvent(mDragTask, - foundDockState)); + if (mLastDropTarget != currentDropTarget) { + mLastDropTarget = currentDropTarget; + EventBus.getDefault().send(new DragDropTargetChangedEvent(mDragTask, + currentDropTarget)); } mDragView.setTranslationX(x); @@ -165,7 +188,7 @@ class RecentsViewTouchHandler { mRv.getContext(), null, null, null); postAnimationTrigger.increment(); EventBus.getDefault().send(new DragEndEvent(mDragTask, mTaskView, mDragView, - mLastDockState, postAnimationTrigger)); + mLastDropTarget, postAnimationTrigger)); postAnimationTrigger.decrement(); break; } diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java index a0713d7defc0..7f5c768d870f 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java @@ -63,23 +63,6 @@ public class TaskStackLayoutAlgorithm { Context mContext; - /* - +-------------------+ - | SEARCH | - +-------------------+ - |+-----------------+| - || FREEFORM || - || || - || || - |+-----------------+| - | +-----------+ | - | +---------------+ | - | | | | - |+-----------------+| - || STACK || - +-------------------+ - */ - // The task bounds (untransformed) for layout. This rect is anchored at mTaskRoot. public Rect mTaskRect = new Rect(); // The freeform workspace bounds, inset from the top by the search bar, and is a fixed height @@ -165,8 +148,8 @@ public class TaskStackLayoutAlgorithm { }, new ParametricCurve.ParametricCurveFunction() { @Override public float f(float p) { - // Don't scale when there are freeform tasks - if (mNumFreeformTasks > 0) { + SystemServicesProxy ssp = Recents.getSystemServices(); + if (ssp.hasFreeformWorkspaceSupport()) { return 1f; } @@ -196,6 +179,7 @@ public class TaskStackLayoutAlgorithm { */ public void initialize(Rect taskStackBounds) { SystemServicesProxy ssp = Recents.getSystemServices(); + RecentsConfiguration config = Recents.getConfiguration(); int widthPadding = (int) (config.taskStackWidthPaddingPct * taskStackBounds.width()); int heightPadding = mContext.getResources().getDimensionPixelSize( @@ -262,9 +246,7 @@ public class TaskStackLayoutAlgorithm { * in the stack. */ void update(TaskStack stack) { - if (DEBUG) { - Log.d(TAG, "update"); - } + SystemServicesProxy ssp = Recents.getSystemServices(); // Clear the progress map mTaskProgressMap.clear(); @@ -292,10 +274,10 @@ public class TaskStackLayoutAlgorithm { mNumStackTasks = stackTasks.size(); mNumFreeformTasks = freeformTasks.size(); + float pAtBackMostTaskTop = 0; + float pAtFrontMostTaskTop = pAtBackMostTaskTop; if (!stackTasks.isEmpty()) { // Update the for each task from back to front. - float pAtBackMostTaskTop = 0; - float pAtFrontMostTaskTop = pAtBackMostTaskTop; int taskCount = stackTasks.size(); for (int i = 0; i < taskCount; i++) { Task task = stackTasks.get(i); @@ -307,7 +289,7 @@ public class TaskStackLayoutAlgorithm { if (i < (taskCount - 1)) { // Increment the peek height - float pPeek = task.group.isFrontMostTask(task) ? + float pPeek = task.group == null || task.group.isFrontMostTask(task) ? mBetweenAffiliationPOffset : mWithinAffiliationPOffset; pAtFrontMostTaskTop += pPeek; } @@ -318,10 +300,12 @@ public class TaskStackLayoutAlgorithm { // Set the stack end scroll progress to the point at which the bottom of the front-most // task is aligned to the bottom of the stack mMaxScrollP = alignToStackBottom(pAtFrontMostTaskTop, - mStackBottomPOffset + mTaskHeightPOffset); + mStackBottomPOffset + (ssp.hasFreeformWorkspaceSupport() ? + mTaskHalfHeightPOffset : mTaskHeightPOffset)); // Basically align the back-most task such that the last two tasks would be visible mMinScrollP = alignToStackBottom(pAtBackMostTaskTop, - mStackBottomPOffset + mTaskHeightPOffset); + mStackBottomPOffset + (ssp.hasFreeformWorkspaceSupport() ? + mTaskHalfHeightPOffset : mTaskHeightPOffset)); } else { // When there is a single item, then just make all the stack progresses the same mMinScrollP = mMaxScrollP = 0; @@ -379,7 +363,7 @@ public class TaskStackLayoutAlgorithm { if (progress < 0) { break; } - boolean isFrontMostTaskInGroup = task.group.isFrontMostTask(task); + boolean isFrontMostTaskInGroup = task.group == null || task.group.isFrontMostTask(task); if (isFrontMostTaskInGroup) { float scaleAtP = sCurve.pToScale(progress); int scaleYOffsetAtP = (int) (((1f - scaleAtP) * taskHeight) / 2); @@ -432,12 +416,13 @@ public class TaskStackLayoutAlgorithm { /** Update/get the transform */ public TaskViewTransform getStackTransform(float taskProgress, float stackScroll, TaskViewTransform transformOut, TaskViewTransform prevTransform) { + SystemServicesProxy ssp = Recents.getSystemServices(); - if (mNumStackTasks == 1) { + if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) { // Center the task in the stack, changing the scale will not follow the curve, but just // modulate some values directly float pTaskRelative = mMinScrollP - stackScroll; - float scale = (mNumFreeformTasks > 0) ? 1f : SINGLE_TASK_SCALE; + float scale = ssp.hasFreeformWorkspaceSupport() ? 1f : SINGLE_TASK_SCALE; int topOffset = (mCurrentStackRect.top - mTaskRect.top) + (mCurrentStackRect.height() - mTaskRect.height()) / 2; transformOut.scale = scale; diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java index 14d75a85000d..fc3a6819c953 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -30,6 +30,8 @@ import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; import android.widget.FrameLayout; import com.android.systemui.R; import com.android.systemui.recents.Constants; @@ -42,6 +44,10 @@ import com.android.systemui.recents.events.activity.PackagesChangedEvent; import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent; import com.android.systemui.recents.events.ui.DismissTaskViewEvent; import com.android.systemui.recents.events.ui.UserInteractionEvent; +import com.android.systemui.recents.events.ui.dragndrop.DragDropTargetChangedEvent; +import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent; +import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent; +import com.android.systemui.recents.events.ui.dragndrop.DragStartInitializeDropTargetsEvent; import com.android.systemui.recents.events.ui.focus.DismissFocusedTaskViewEvent; import com.android.systemui.recents.events.ui.focus.FocusNextTaskViewEvent; import com.android.systemui.recents.events.ui.focus.FocusPreviousTaskViewEvent; @@ -58,6 +64,8 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; +import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; import static android.app.ActivityManager.StackId.INVALID_STACK_ID; @@ -110,8 +118,10 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal LayoutInflater mInflater; boolean mLayersDisabled; + Interpolator mFastOutSlowInInterpolator; + // A convenience update listener to request updating clipping of tasks - ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener = + private ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { @@ -119,6 +129,21 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } }; + // The drop targets for a task drag + private DropTarget mFreeformWorkspaceDropTarget = new DropTarget() { + @Override + public boolean acceptsDrop(int x, int y, int width, int height) { + return mLayoutAlgorithm.mFreeformRect.contains(x, y); + } + }; + + private DropTarget mStackDropTarget = new DropTarget() { + @Override + public boolean acceptsDrop(int x, int y, int width, int height) { + return mLayoutAlgorithm.mCurrentStackRect.contains(x, y); + } + }; + public TaskStackView(Context context, TaskStack stack) { super(context); // Set the stack first @@ -130,6 +155,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mStackScroller = new TaskStackViewScroller(context, mLayoutAlgorithm); mStackScroller.setCallbacks(this); mTouchHandler = new TaskStackViewTouchHandler(context, this, mStackScroller); + mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, + com.android.internal.R.interpolator.fast_out_slow_in); int taskBarDismissDozeDelaySeconds = getResources().getInteger( R.integer.recents_task_bar_dismiss_delay_seconds); @@ -390,7 +417,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Pick up all the freeform tasks int firstVisStackIndex = isValidVisibleStackRange ? visibleStackRange[0] : 0; - for (int i = mStack.getTaskCount() - 1; i > firstVisStackIndex; i--) { + for (int i = mStack.getTaskCount() - 1; i >= firstVisStackIndex; i--) { Task task = tasks.get(i); if (!task.isFreeformTask()) { continue; @@ -411,6 +438,24 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Animate the task into place tv.updateViewPropertiesToTaskTransform(transform, mStackViewsAnimationDuration, mRequestUpdateClippingListener); + + // Reattach it in the right z order + detachViewFromParent(tv); + int insertIndex = -1; + int taskIndex = mStack.indexOfTask(task); + taskViews = getTaskViews(); + taskViewCount = taskViews.size(); + for (int j = 0; j < taskViewCount; j++) { + Task tvTask = taskViews.get(j).getTask(); + if (taskIndex < mStack.indexOfTask(tvTask)) { + insertIndex = j; + break; + } + } + attachViewToParent(tv, insertIndex, tv.getLayoutParams()); + + // Update the task views list after adding the new task view + updateTaskViewsList(); } // Pick up all the newly visible children and update all the existing children @@ -514,7 +559,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } /** Updates the min and max virtual scroll bounds */ - void updateMinMaxScroll(boolean boundScrollToNewMinMax) { + void updateLayout(boolean boundScrollToNewMinMax) { // Compute the min and max scroll values mLayoutAlgorithm.update(mStack); @@ -700,21 +745,21 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mLayoutAlgorithm.initialize(taskStackBounds); // Update the scroll bounds - updateMinMaxScroll(false); + updateLayout(false); } /** - * This is ONLY used from AlternateRecentsComponent to update the dummy stack view for purposes + * This is ONLY used from the Recents component to update the dummy stack view for purposes * of getting the task rect to animate to. */ - public void updateMinMaxScrollForStack(TaskStack stack) { + public void updateLayoutForStack(TaskStack stack) { mStack = stack; - updateMinMaxScroll(false); + updateLayout(false); } /** * Computes the maximum number of visible tasks and thumbnails. Requires that - * updateMinMaxScrollForStack() is called first. + * updateLayoutForStack() is called first. */ public TaskStackLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() { return mLayoutAlgorithm.computeStackVisibilityReport(mStack.getTasks()); @@ -824,15 +869,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal // Set the task focused state without requesting view focus, and leave the focus animations // until after the enter-animation - if (!Constants.DebugFlags.App.EnableFastToggleRecents && launchTargetTask != null) { - setFocusedTask(mStack.indexOfTask(launchTargetTask), false /* scrollToTask */, - false /* animated */, false /* requestViewFocus */); - } else { - RecentsConfiguration config = Recents.getConfiguration(); - RecentsActivityLaunchState launchState = config.getLaunchState(); - int taskOffset = launchState.launchedFromHome ? -1 : -2; - setFocusedTask(mStack.getTaskCount() + taskOffset, false /* scrollToTask */, - false /* animated */, false /* requestViewFocus */); + RecentsConfiguration config = Recents.getConfiguration(); + RecentsActivityLaunchState launchState = config.getLaunchState(); + int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getTaskCount()); + if (focusedTaskIndex != -1) { + setFocusedTask(focusedTaskIndex, false /* scrollToTask */, false /* animated */, + false /* requestViewFocus */); } // Start dozing @@ -1005,7 +1047,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } // Update the min/max scroll and animate other task views into their new positions - updateMinMaxScroll(true); + updateLayout(true); if (wasFrontMostTask) { // Since the max scroll progress is offset from the bottom of the stack, just scroll @@ -1031,7 +1073,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } // Update the min/max scroll and animate other task views into their new positions - updateMinMaxScroll(true); + updateLayout(true); // Animate all the tasks into place requestSynchronizeStackViewsWithModel(200); @@ -1091,7 +1133,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mLayoutAlgorithm.updateTaskOffsets(mStack.getTasks()); // Scroll the item to the top of the stack (sans-peek) rect so that we can see it better - updateMinMaxScroll(false); + updateLayout(false); float overlapHeight = mLayoutAlgorithm.getTaskOverlapHeight(); setStackScrollRaw((int) (newStack.indexOfTask(filteredTask) * overlapHeight)); boundScrollRaw(); @@ -1120,7 +1162,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal mLayoutAlgorithm.updateTaskOffsets(mStack.getTasks()); // Restore the stashed scroll - updateMinMaxScroll(false); + updateLayout(false); setStackScrollRaw(mStashedScroll); boundScrollRaw(); @@ -1308,6 +1350,79 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal } } + public final void onBusEvent(DragStartEvent event) { + if (event.task.isFreeformTask()) { + // Animate to the front of the stack + mStackScroller.animateScroll(mStackScroller.getStackScroll(), + mLayoutAlgorithm.mInitialScrollP, null); + } + } + + public final void onBusEvent(DragStartInitializeDropTargetsEvent event) { + SystemServicesProxy ssp = Recents.getSystemServices(); + if (ssp.hasFreeformWorkspaceSupport()) { + event.handler.registerDropTargetForCurrentDrag(mStackDropTarget); + event.handler.registerDropTargetForCurrentDrag(mFreeformWorkspaceDropTarget); + } + } + + public final void onBusEvent(DragDropTargetChangedEvent event) { + // TODO: Animate the freeform workspace background etc. + } + + public final void onBusEvent(final DragEndEvent event) { + if (event.dropTarget != mFreeformWorkspaceDropTarget && + event.dropTarget != mStackDropTarget) { + return; + } + if (event.task.isFreeformTask() && event.dropTarget == mFreeformWorkspaceDropTarget) { + // TODO: Animate back into view + return; + } + if (!event.task.isFreeformTask() && event.dropTarget == mStackDropTarget) { + // TODO: Animate back into view + return; + } + + // Move the task to the right position in the stack (ie. the front of the stack if freeform + // or the front of the stack if fullscreen). Note, we MUST move the tasks before we update + // their stack ids, otherwise, the keys will have changed. + if (event.dropTarget == mFreeformWorkspaceDropTarget) { + mStack.moveTaskToStack(event.task, FREEFORM_WORKSPACE_STACK_ID); + updateLayout(true); + } else if (event.dropTarget == mStackDropTarget) { + mStack.moveTaskToStack(event.task, FULLSCREEN_WORKSPACE_STACK_ID); + updateLayout(true); + } + + event.postAnimationTrigger.increment(); + event.postAnimationTrigger.addLastDecrementRunnable(new Runnable() { + @Override + public void run() { + SystemServicesProxy ssp = Recents.getSystemServices(); + ssp.moveTaskToStack(event.task.key.id, event.task.key.stackId); + } + }); + + // Animate the drag view to the new position + mLayoutAlgorithm.getStackTransform(event.task, mStackScroller.getStackScroll(), + mTmpTransform, null); + event.dragView.animate() + .scaleX(mTmpTransform.scale) + .scaleY(mTmpTransform.scale) + .translationX((mLayoutAlgorithm.mTaskRect.left - event.dragView.getLeft()) + + mTmpTransform.translationX) + .translationY((mLayoutAlgorithm.mTaskRect.top - event.dragView.getTop()) + + mTmpTransform.translationY) + .setDuration(175) + .setInterpolator(mFastOutSlowInInterpolator) + .withEndAction(event.postAnimationTrigger.decrementAsRunnable()) + .start(); + + // Animate the other views into place + requestSynchronizeStackViewsWithModel(175); + } + /** * Removes the task from the stack, and updates the focus to the next task in the stack if the * removed TaskView was focused. diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java index 57cb599162c5..dd1474d9633e 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -51,6 +51,7 @@ import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; import com.android.systemui.statusbar.phone.PhoneStatusBar; /* A task view */ @@ -708,6 +709,7 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, @Override public void onTaskDataLoaded() { + SystemServicesProxy ssp = Recents.getSystemServices(); RecentsConfiguration config = Recents.getConfiguration(); if (mThumbnailView != null && mHeaderView != null) { // Bind each of the views to the new task data @@ -715,7 +717,14 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, mHeaderView.rebindToTask(mTask); // Rebind any listeners mActionButtonView.setOnClickListener(this); - setOnLongClickListener(config.hasDockedTasks ? null : this); + + // Only enable long-click if we have a freeform workspace to drag to/from, or if we + // aren't already docked + if (ssp.hasFreeformWorkspaceSupport() || !config.hasDockedTasks) { + setOnLongClickListener(this); + } else { + setOnLongClickListener(null); + } } mTaskDataLoaded = true; } @@ -759,15 +768,21 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, // Start listening for drag events setClipViewInStack(false); - final int width = (int) (getScaleX() * getWidth()); - final int height = (int) (getScaleY() * getHeight()); + final float finalScale = getScaleX() * 1.05f; + final int width = getWidth(); + final int height = getHeight(); Bitmap dragBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(dragBitmap); - c.scale(getScaleX(), getScaleY()); mThumbnailView.draw(c); mHeaderView.draw(c); c.setBitmap(null); + // The downTouchPos is relative to the currently transformed TaskView, but we will be + // dragging a copy of the full task view, which makes it easier for us to animate them + // when the user drops + mDownTouchPos.x += ((1f - getScaleX()) * width) / 2; + mDownTouchPos.y += ((1f - getScaleY()) * height) / 2; + // Initiate the drag final DragView dragView = new DragView(getContext(), dragBitmap, mDownTouchPos); dragView.setOutlineProvider(new ViewOutlineProvider() { @@ -776,6 +791,8 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, outline.setRect(0, 0, width, height); } }); + dragView.setScaleX(getScaleX()); + dragView.setScaleY(getScaleY()); dragView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { @@ -785,8 +802,8 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, dragView.setElevation(getElevation()); dragView.setTranslationZ(getTranslationZ()); dragView.animate() - .scaleX(1.05f) - .scaleY(1.05f) + .scaleX(finalScale) + .scaleY(finalScale) .setDuration(175) .setInterpolator(mFastOutSlowInInterpolator) .start(); @@ -807,18 +824,19 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, /**** Events ****/ public final void onBusEvent(DragEndEvent event) { - event.postAnimationTrigger.addLastDecrementRunnable(new Runnable() { - @Override - public void run() { - // If docked state == null: - // Animate the drag view back from where it is, to the view location, then after it returns, - // update the clip state - setClipViewInStack(true); + if (!(event.dropTarget instanceof TaskStack.DockState)) { + event.postAnimationTrigger.addLastDecrementRunnable(new Runnable() { + @Override + public void run() { + // Show this task view + setVisibility(View.VISIBLE); - // Show this task view - setVisibility(View.VISIBLE); - } - }); + // Animate the drag view back from where it is, to the view location, then after + // it returns, update the clip state + setClipViewInStack(true); + } + }); + } EventBus.getDefault().unregister(this); } } diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java index 9441d88f2504..19872144bfe3 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -168,24 +168,6 @@ public class ConditionProviders extends ManagedServices { } } - public void requestConditions(IConditionListener callback, int relevance) { - synchronized(mMutex) { - if (DEBUG) Slog.d(TAG, "requestConditions callback=" + callback - + " relevance=" + Condition.relevanceToString(relevance)); - if (callback == null) return; - relevance = relevance & (Condition.FLAG_RELEVANT_NOW | Condition.FLAG_RELEVANT_ALWAYS); - if (relevance != 0) { - mListeners.put(callback.asBinder(), callback); - requestConditionsLocked(relevance); - } else { - mListeners.remove(callback.asBinder()); - if (mListeners.isEmpty()) { - requestConditionsLocked(0); - } - } - } - } - private Condition[] validateConditions(String pkg, Condition[] conditions) { if (conditions == null || conditions.length == 0) return null; final int N = conditions.length; @@ -382,25 +364,6 @@ public class ConditionProviders extends ManagedServices { return info == null ? null : (IConditionProvider) info.service; } - private void requestConditionsLocked(int flags) { - for (ManagedServiceInfo info : mServices) { - final IConditionProvider provider = provider(info); - if (provider == null) continue; - // clear all stored conditions from this provider that we no longer care about - for (int i = mRecords.size() - 1; i >= 0; i--) { - final ConditionRecord r = mRecords.get(i); - if (r.info != info) continue; - if (r.subscribed) continue; - mRecords.remove(i); - } - try { - provider.onRequestConditions(flags); - } catch (RemoteException e) { - Slog.w(TAG, "Error requesting conditions from " + info.component, e); - } - } - } - private static class ConditionRecord { public final Uri id; public final ComponentName component; diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java index c2e434979c01..cee9ec8a026d 100644 --- a/services/core/java/com/android/server/notification/ZenModeConditions.java +++ b/services/core/java/com/android/server/notification/ZenModeConditions.java @@ -59,10 +59,6 @@ public class ZenModeConditions implements ConditionProviders.Callback { pw.print(prefix); pw.print("mSubscriptions="); pw.println(mSubscriptions); } - public void requestConditions(IConditionListener callback, int relevance) { - mConditionProviders.requestConditions(callback, relevance); - } - public void evaluateConfig(ZenModeConfig config, boolean processSubscriptions) { if (config == null) return; if (config.manualRule != null && config.manualRule.condition != null diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 5110c999ac36..b3f9ddb2eef8 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -7081,6 +7081,11 @@ public class WindowManagerService extends IWindowManager.Stub Slog.w(TAG, "startPositioningLocked: Bad window " + win); return false; } + if (win.mInputChannel == null) { + Slog.wtf(TAG, "startPositioningLocked: " + win + " has no input channel, " + + " probably being removed"); + return false; + } final DisplayContent displayContent = win.getDisplayContent(); if (displayContent == null) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 2b686b2ea750..3b0fc4f186a4 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -70,6 +70,8 @@ import android.view.Gravity; import android.view.IApplicationToken; import android.view.IWindow; import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; import android.view.View; import android.view.ViewTreeObserver; import android.view.WindowManager; @@ -1282,6 +1284,20 @@ final class WindowState implements WindowManagerPolicy.WindowState { mConfigHasChanged = false; } + private final class DeadWindowEventReceiver extends InputEventReceiver { + DeadWindowEventReceiver(InputChannel inputChannel) { + super(inputChannel, mService.mH.getLooper()); + } + @Override + public void onInputEvent(InputEvent event) { + finishInputEvent(event, true); + } + } + /** + * Dummy event receiver for windows that died visible. + */ + private DeadWindowEventReceiver mDeadWindowEventReceiver; + void openInputChannel(InputChannel outInputChannel) { if (mInputChannel != null) { throw new IllegalStateException("Window already has an input channel."); @@ -1295,22 +1311,31 @@ final class WindowState implements WindowManagerPolicy.WindowState { mClientChannel.transferTo(outInputChannel); mClientChannel.dispose(); mClientChannel = null; + } else { + // If the window died visible, we setup a dummy input channel, so that taps + // can still detected by input monitor channel, and we can relaunch the app. + // Create dummy event receiver that simply reports all events as handled. + mDeadWindowEventReceiver = new DeadWindowEventReceiver(mClientChannel); } mService.mInputManager.registerInputChannel(mInputChannel, mInputWindowHandle); } void disposeInputChannel() { - if (mClientChannel != null) { - mClientChannel.dispose(); - mClientChannel = null; + if (mDeadWindowEventReceiver != null) { + mDeadWindowEventReceiver.dispose(); + mDeadWindowEventReceiver = null; } + + // unregister server channel first otherwise it complains about broken channel if (mInputChannel != null) { mService.mInputManager.unregisterInputChannel(mInputChannel); - mInputChannel.dispose(); mInputChannel = null; } - + if (mClientChannel != null) { + mClientChannel.dispose(); + mClientChannel = null; + } mInputWindowHandle.inputChannel = null; } |