<uses-permission android:name="android.permission.ACCESS_SUPERUSER"/>
<uses-permission android:name="android.permission.NFC"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="com.cyanogenmod.filemanager.permissions.READ_THEME"/>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
+ <service android:name=".providers.secure.SecureCacheCleanupService">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ <action android:name="android.intent.action.QUICKBOOT_POWERON" />
+ <action android:name="com.cyanogenmod.filemanager.ACTION_START_CLEANUP"/>
+ </intent-filter>
+ </service>
</application>
<string name="secure_storage_unlock_validation_length">Password must have at least <xliff:g id="characters">%1$d</xliff:g> characters.</string>
<!-- Secure Storage unlock validation, equal -->
<string name="secure_storage_unlock_validation_equals">Passwords do not match.</string>
+ <!-- Secure storage open file warning -->
+ <string name="secure_storage_open_file_warning">This will copy the file out to a temporary
+ unencrypted location. This will be cleared after 1 hour.</string>
+
<!-- Print messages -->
<!-- Unsupported document format -->
<string name="welcome_msg">Welcome to the CyanogenMod file manager.\n\nThis app allows you to explore the file system and do operations that could break your device. To prevent damage, the app will start in a safe, low-privileged mode.\n\nYou can access the advanced, full-privileged mode via Settings. It\'s your responsibility to ensure that an operation doesn\'t break your system.\n\nThe CyanogenMod Team</string>
<string name="activity_not_found_exception">Couldn\'t find an app to open this file</string>
+
</resources>
import com.cyanogenmod.filemanager.preferences.FileManagerSettings;
import com.cyanogenmod.filemanager.preferences.ObjectStringIdentifier;
import com.cyanogenmod.filemanager.preferences.Preferences;
+import com.cyanogenmod.filemanager.providers.secure.SecureCacheCleanupService;
import com.cyanogenmod.filemanager.service.MimeTypeIndexService;
import com.cyanogenmod.filemanager.ui.ThemeManager;
import com.cyanogenmod.filemanager.ui.ThemeManager.Theme;
MimeTypeIndexService.indexFileRoot(this, externalStorage.getAbsolutePath());
MimeTypeIndexService.indexFileRoot(this, Environment.getRootDirectory().getAbsolutePath());
+ // Schedule in case not scheduled (i.e. never booted with this app on device
+ SecureCacheCleanupService.scheduleCleanup(getApplicationContext());
+
}
/**
// Ignored
}
+ @Override
+ public void onCancel(){
+ // nop
+ }
+
/**
* {@inheritDoc}
*/
}
}
+ @Override
+ public void onCancel() {
+ // nop
+ }
+
/**
* Method that returns to previous activity.
*
* @param o The object where to navigate to
*/
void onNavigateTo(Object o);
+
+ /**
+ * Invoked on action cancel
+ */
+ void onCancel();
}
--- /dev/null
+/*
+* Copyright (C) 2015 The CyanogenMod 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.cyanogenmod.filemanager.providers.secure;
+
+import java.io.File;
+
+/**
+ * ISecureChoiceCompleteListener
+ * <pre>
+ * This interface is to notify the caller
+ * in this case
+ * {@link com.cyanogenmod.filemanager.ui.policy.IntentsActionPolicy}
+ * that the operation is complete
+ * </pre>
+ */
+public interface ISecureChoiceCompleteListener {
+ public void onComplete(File cacheFile);
+ public void onCancelled();
+}
--- /dev/null
+/*
+* Copyright (C) 2015 The CyanogenMod 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.cyanogenmod.filemanager.providers.secure;
+
+import android.app.AlarmManager;
+import android.app.IntentService;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import com.cyanogenmod.filemanager.model.FileSystemObject;
+import com.cyanogenmod.filemanager.util.FileHelper;
+
+import java.io.File;
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * SecureCacheCleanupService
+ * <pre>
+ * Service that cleans up cache
+ * </pre>
+ *
+ * @see {@link android.app.IntentService}
+ */
+public class SecureCacheCleanupService extends IntentService {
+
+ // Constants
+ private static final String ACTION_START = "com.cyanogenmod.filemanager.ACTION_START_CLEANUP";
+ private static final String NAME = "cleanup-service";
+
+ /**
+ * Creates an IntentService. Invoked by your subclass's constructor.
+ */
+ public SecureCacheCleanupService() {
+ super(NAME);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+ scheduleCleanup(this);
+ } else if (ACTION_START.equals(action)) {
+ cleanupOperation();
+ }
+ }
+
+ private void cleanupOperation() {
+ File cacheDir = new File(getExternalCacheDir(), SecureChoiceClickListener.CACHE_DIR);
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(System.currentTimeMillis());
+ calendar.add(Calendar.HOUR, -1);
+ Date cutoff = calendar.getTime();
+
+ if (!cacheDir.exists()) {
+ return;
+ }
+
+ // Get list of files
+ File[] files = cacheDir.listFiles();
+
+ if (files == null) {
+ return;
+ }
+
+ // Delete all, won't run if list is empty
+ for (File file : files) {
+ FileSystemObject fso = FileHelper.createFileSystemObject(file);
+ Date lastAccessDate = fso.getLastAccessedTime();
+ if (lastAccessDate.before(cutoff)) {
+ file.delete();
+ }
+ }
+
+ // Check again after deletion
+ files = cacheDir.listFiles();
+
+ // If no files, cancel alarm
+ if (files == null || files.length < 1) {
+ cancelAlarm(this);
+ }
+
+ }
+
+ /**
+ * Schedule a cleanup alarm
+ *
+ * @param context {@link android.content.Context}
+ *
+ * @throws IllegalArgumentException {@link java.lang.IllegalArgumentException}
+ */
+ public static void scheduleCleanup(Context context) throws IllegalArgumentException {
+ if (context == null) {
+ throw new IllegalArgumentException("'context' cannot be null!");
+ }
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ Intent intent = new Intent(context, SecureCacheCleanupService.class);
+ intent.setAction(ACTION_START);
+ PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, 1000,
+ AlarmManager.INTERVAL_HOUR, pendingIntent);
+ }
+
+ /**
+ * Cancel a cleanup alarm
+ *
+ * @param context {@link android.content.Context}
+ *
+ * @throws IllegalArgumentException {@link java.lang.IllegalArgumentException}
+ */
+ public static void cancelAlarm(Context context) throws IllegalArgumentException {
+ if (context == null) {
+ throw new IllegalArgumentException("'context' cannot be null!");
+ }
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ Intent intent = new Intent(context, SecureCacheCleanupService.class);
+ intent.setAction(ACTION_START);
+ PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ alarmManager.cancel(pendingIntent);
+ }
+}
--- /dev/null
+/*
+* Copyright (C) 2015 The CyanogenMod 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.cyanogenmod.filemanager.providers.secure;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import com.cyanogenmod.filemanager.model.FileSystemObject;
+import com.cyanogenmod.filemanager.ui.policy.CopyMoveActionPolicy;
+import com.cyanogenmod.filemanager.ui.policy.CopyMoveActionPolicy.LinkedResource;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * SecureChoiceClickListener
+ * <pre>
+ * This listens for the secure choice user selection
+ * </pre>
+ *
+ * @see {@link android.content.DialogInterface.OnClickListener}
+ */
+public class SecureChoiceClickListener implements DialogInterface.OnClickListener {
+
+ // Constants
+ /* package */ static final String CACHE_DIR = ".opened-files";
+
+ // Members
+ private Context mContext;
+ private FileSystemObject mFso;
+ private ISecureChoiceCompleteListener mListener;
+
+ /**
+ * Constructor
+ *
+ * @param context {@link android.content.Context}
+ * @param fso {@link com.cyanogenmod.filemanager.model.FileSystemObject}
+ *
+ * @throws IllegalArgumentException {@link java.lang.IllegalArgumentException}
+ */
+ public SecureChoiceClickListener(Context context, FileSystemObject fso,
+ ISecureChoiceCompleteListener listener) throws IllegalArgumentException {
+ if (context == null) {
+ throw new IllegalArgumentException("'context' cannot be null!");
+ }
+ if (fso == null) {
+ throw new IllegalArgumentException("'fso' cannot be null!");
+ }
+ if (listener == null) {
+ throw new IllegalArgumentException("'listener' cannot be null!");
+ }
+ mContext = context;
+ mFso = fso;
+ mListener = listener;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case AlertDialog.BUTTON_POSITIVE:
+ performUserFlow();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void performUserFlow() {
+ List<LinkedResource> selection = new ArrayList<LinkedResource>();
+ File hiddenCacheDirectory = new File(mContext.getExternalCacheDir(), CACHE_DIR);
+ // Check if the hidden directory exists
+ if (!hiddenCacheDirectory.exists()) {
+ hiddenCacheDirectory.mkdirs();
+ }
+ final File tmpFso = new File(hiddenCacheDirectory, mFso.getName());
+ selection.add(new LinkedResource(new File(mFso.getFullPath()), tmpFso));
+ CopyMoveActionPolicy.copyFileSystemObjects(mContext, selection,
+ new SecureChoiceSelectionListener(tmpFso),
+ new SecureChoiceRefreshListener(tmpFso, mListener));
+
+ }
+}
--- /dev/null
+/*
+* Copyright (C) 2015 The CyanogenMod 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.cyanogenmod.filemanager.providers.secure;
+
+import com.cyanogenmod.filemanager.listeners.OnRequestRefreshListener;
+
+import java.io.File;
+
+/**
+ * SecureChoiceRefreshListener
+ * <pre>
+ * This is just a chained callback
+ * </pre>
+ *
+ * @see {@link com.cyanogenmod.filemanager.listeners.OnRequestRefreshListener}
+ */
+/* package */ class SecureChoiceRefreshListener implements OnRequestRefreshListener {
+
+ // Members
+ private File mCacheFile;
+ private ISecureChoiceCompleteListener mListener;
+
+ /**
+ * Constructor
+ *
+ * @param listener
+ * {@link com.cyanogenmod.filemanager.providers.secure.ISecureChoiceCompleteListener}
+ *
+ * @throws IllegalArgumentException {@link java.lang.IllegalArgumentException}
+ */
+ public SecureChoiceRefreshListener(File cacheFile, ISecureChoiceCompleteListener listener)
+ throws IllegalArgumentException {
+ if (cacheFile == null) {
+ throw new IllegalArgumentException("'cacheFile' cannot be null!");
+ }
+ if (listener == null) {
+ throw new IllegalArgumentException("'listener' cannot be null!");
+ }
+ mCacheFile = cacheFile;
+ mListener = listener;
+ }
+
+ @Override
+ public void onRequestRefresh(Object o, boolean clearSelection) {
+ mListener.onComplete(mCacheFile);
+ }
+
+ @Override
+ public void onRequestBookmarksRefresh() {
+
+ }
+
+ @Override
+ public void onRequestRemove(Object o, boolean clearSelection) {
+
+ }
+
+ @Override
+ public void onNavigateTo(Object o) {
+
+ }
+
+ @Override
+ public void onCancel() {
+ mListener.onCancelled();
+ }
+}
--- /dev/null
+/*
+* Copyright (C) 2015 The CyanogenMod 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.cyanogenmod.filemanager.providers.secure;
+
+import com.cyanogenmod.filemanager.listeners.OnSelectionListener;
+import com.cyanogenmod.filemanager.model.FileSystemObject;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * SecureChoiceSelectionListener
+ * <pre>
+ * This is something that the copy/move pipeline needs in order to
+ * pass a conditional check in {@link com.cyanogenmod.filemanager.ui.policy
+ * .CopyMoveActionPolicy}
+ * </pre>
+ *
+ * @see {@link com.cyanogenmod.filemanager.listeners.OnSelectionListener}
+ */
+/* package */ class SecureChoiceSelectionListener implements OnSelectionListener {
+
+ // Members
+ private File mFile;
+
+ /**
+ * Constructor
+ *
+ * @param fso {@link java.io.File}
+ * @throws IllegalArgumentException {@link java.lang.IllegalArgumentException}
+ */
+ public SecureChoiceSelectionListener(File fso) throws IllegalArgumentException {
+ if (fso == null) {
+ throw new IllegalArgumentException("'fso' cannot be null!");
+ }
+ mFile = fso;
+ }
+
+ @Override
+ public void onToggleSelection(FileSystemObject fso) {
+
+ }
+
+ @Override
+ public void onDeselectAll() {
+
+ }
+
+ @Override
+ public void onSelectAllVisibleItems() {
+
+ }
+
+ @Override
+ public void onDeselectAllVisibleItems() {
+
+ }
+
+ @Override
+ public List<FileSystemObject> onRequestSelectedFiles() {
+ return null;
+ }
+
+ @Override
+ public List<FileSystemObject> onRequestCurrentItems() {
+ return null;
+ }
+
+ @Override
+ public String onRequestCurrentDir() {
+ return mFile.getParent();
+ }
+
+}
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
-import android.content.DialogInterface.OnCancelListener;
import android.text.Html;
import android.text.Spanned;
import com.cyanogenmod.filemanager.R;
-import com.cyanogenmod.filemanager.console.CancelledOperationException;
import com.cyanogenmod.filemanager.console.Console;
import com.cyanogenmod.filemanager.console.NoSuchFileOrDirectory;
import com.cyanogenmod.filemanager.console.RelaunchableException;
if (mDstConsole != null) {
mDstConsole.onCancel();
}
+ if (mOnRequestRefreshListener != null) {
+ mOnRequestRefreshListener.onCancel();
+ }
refreshUIAfterCompletion();
}
package com.cyanogenmod.filemanager.ui.policy;
import android.content.ComponentName;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnDismissListener;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
-
import com.cyanogenmod.filemanager.R;
import com.cyanogenmod.filemanager.activities.ShortcutActivity;
import com.cyanogenmod.filemanager.console.secure.SecureConsole;
import com.cyanogenmod.filemanager.model.RegularFile;
import com.cyanogenmod.filemanager.providers.SecureResourceProvider;
import com.cyanogenmod.filemanager.providers.SecureResourceProvider.AuthorizationResource;
+import com.cyanogenmod.filemanager.providers.secure.ISecureChoiceCompleteListener;
+import com.cyanogenmod.filemanager.providers.secure.SecureCacheCleanupService;
+import com.cyanogenmod.filemanager.providers.secure.SecureChoiceClickListener;
import com.cyanogenmod.filemanager.ui.dialogs.AssociationsDialog;
import com.cyanogenmod.filemanager.util.DialogHelper;
import com.cyanogenmod.filemanager.util.ExceptionUtil;
*/
public static void openFileSystemObject(
final Context ctx, final FileSystemObject fso, final boolean choose,
- OnCancelListener onCancelListener, OnDismissListener onDismissListener) {
+ final OnCancelListener onCancelListener, final OnDismissListener onDismissListener) {
try {
// Create the intent to open the file
- Intent intent = new Intent();
+ final Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
+ // [NOTE][MSB]: Short circuit to pop up dialog informing user we need to copy out the
+ // file until we find a better solution.
+ if (fso.isSecure()) {
+ // [TODO][MSB]: Check visible cache for existing file but I need to split up
+ // resolveIntent function properly for this to be successful
+ DialogHelper.createTwoButtonsQuestionDialog(
+ ctx,
+ R.string.ok,
+ R.string.cancel,
+ R.string.warning_title,
+ ctx.getResources().getString(R.string.secure_storage_open_file_warning),
+ new SecureChoiceClickListener(ctx, fso,
+ new ISecureChoiceCompleteListener() {
+ private boolean isCancelled = false;
+ @Override
+ public void onComplete(File cacheFile) {
+ if (isCancelled) {
+ return;
+ }
+ // Schedule cleanup alarm
+ SecureCacheCleanupService.scheduleCleanup(ctx);
+
+ FileSystemObject cacheFso = FileHelper
+ .createFileSystemObject(cacheFile);
+ // Obtain the mime/type and passed it to intent
+ String mime = MimeTypeHelper.getMimeType(ctx, cacheFso);
+ if (mime != null) {
+ intent.setDataAndType(getUriFromFile(ctx, cacheFso),
+ mime);
+ } else {
+ intent.setData(getUriFromFile(ctx, cacheFso));
+ }
+ // Resolve the intent
+ resolveIntent(
+ ctx,
+ intent,
+ choose,
+ createInternalIntents(ctx, cacheFso),
+ 0,
+ R.string.associations_dialog_openwith_title,
+ R.string.associations_dialog_openwith_action,
+ true,
+ onCancelListener,
+ onDismissListener);
+ }
+
+ @Override
+ public void onCancelled() {
+ isCancelled = true;
+ Toast.makeText(ctx, R.string.cancelled_message, Toast
+ .LENGTH_SHORT).show();
+ }
+ }))
+ .show();
+ return;
+ }
+
// Obtain the mime/type and passed it to intent
String mime = MimeTypeHelper.getMimeType(ctx, fso);
if (mime != null) {
ctx,
intent,
choose,
- createInternalIntents(ctx, fso),
+ createInternalIntents(ctx, fso),
0,
R.string.associations_dialog_openwith_title,
R.string.associations_dialog_openwith_action,
// Ignored
}
+ @Override
+ public void onCancel() {
+ // nop
+ }
+
/**
* {@inheritDoc}
*/