mirror of
https://github.com/etesync/android
synced 2024-12-23 15:18:14 +00:00
Add import from vCard (vcf)/iCal (ics)
This commit adds a basic UI for importing contacts and calendars from a file.
This commit is contained in:
parent
24b170a170
commit
f984b76ec6
@ -29,9 +29,11 @@
|
||||
for writing external log files; permission only required for SDK <= 18 because since then,
|
||||
writing to app-private directory doesn't require extra permissions
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="18"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="18"/>
|
||||
|
||||
<!-- Used for external log and vcf import. -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- other permissions -->
|
||||
<!-- android.permission-group.CONTACTS -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
@ -207,8 +209,6 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/log_paths" />
|
||||
</provider>
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -8,8 +8,6 @@
|
||||
|
||||
package com.etesync.syncadapter;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Application;
|
||||
|
@ -40,5 +40,6 @@ public class Constants {
|
||||
public static final int DEFAULT_SYNC_INTERVAL = 4 * 3600; // 4 hours
|
||||
public static final long DEFAULT_RETRY_DELAY = 30 * 60; // 30 minutes
|
||||
|
||||
public final static String KEY_ACCOUNT = "account";
|
||||
public final static String KEY_ACCOUNT = "account",
|
||||
KEY_COLLECTION_INFO = "collectionInfo";
|
||||
}
|
||||
|
410
app/src/main/java/com/etesync/syncadapter/ui/ImportFragment.java
Normal file
410
app/src/main/java/com/etesync/syncadapter/ui/ImportFragment.java
Normal file
@ -0,0 +1,410 @@
|
||||
package com.etesync.syncadapter.ui;
|
||||
|
||||
import android.Manifest;
|
||||
import android.accounts.Account;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.ContactsContract;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.etesync.syncadapter.App;
|
||||
import com.etesync.syncadapter.R;
|
||||
import com.etesync.syncadapter.model.CollectionInfo;
|
||||
import com.etesync.syncadapter.resource.LocalAddressBook;
|
||||
import com.etesync.syncadapter.resource.LocalCalendar;
|
||||
import com.etesync.syncadapter.resource.LocalContact;
|
||||
import com.etesync.syncadapter.resource.LocalEvent;
|
||||
|
||||
import org.apache.commons.codec.Charsets;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
import at.bitfire.ical4android.CalendarStorageException;
|
||||
import at.bitfire.ical4android.Event;
|
||||
import at.bitfire.ical4android.InvalidCalendarException;
|
||||
import at.bitfire.vcard4android.Contact;
|
||||
import at.bitfire.vcard4android.ContactsStorageException;
|
||||
import lombok.Cleanup;
|
||||
import lombok.ToString;
|
||||
|
||||
import static com.etesync.syncadapter.Constants.KEY_ACCOUNT;
|
||||
import static com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO;
|
||||
|
||||
public class ImportFragment extends DialogFragment {
|
||||
private static final int REQUEST_CODE = 6384; // onActivityResult request
|
||||
|
||||
private static final String TAG_PROGRESS_MAX = "progressMax";
|
||||
|
||||
private Account account;
|
||||
private CollectionInfo info;
|
||||
private File importFile;
|
||||
|
||||
public static ImportFragment newInstance(Account account, CollectionInfo info) {
|
||||
ImportFragment frag = new ImportFragment();
|
||||
Bundle args = new Bundle(1);
|
||||
args.putParcelable(KEY_ACCOUNT, account);
|
||||
args.putSerializable(KEY_COLLECTION_INFO, info);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setCancelable(false);
|
||||
setRetainInstance(true);
|
||||
|
||||
account = getArguments().getParcelable(KEY_ACCOUNT);
|
||||
info = (CollectionInfo) getArguments().getSerializable(KEY_COLLECTION_INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
chooseFile();
|
||||
} else {
|
||||
ImportResult data = new ImportResult();
|
||||
data.e = new Exception(getString(R.string.import_permission_required));
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(ResultFragment.newInstance(data), null)
|
||||
.commitAllowingStateLoss();
|
||||
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
private void requestPermissions() {
|
||||
requestPermissions(new String[]{
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
super.onCreateDialog(savedInstanceState);
|
||||
ProgressDialog progress = new ProgressDialog(getActivity());
|
||||
progress.setTitle(R.string.import_dialog_title);
|
||||
progress.setMessage(getString(R.string.import_dialog_loading_file));
|
||||
progress.setCanceledOnTouchOutside(false);
|
||||
progress.setIndeterminate(false);
|
||||
progress.setIcon(R.drawable.ic_import_export_black);
|
||||
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
requestPermissions();
|
||||
} else {
|
||||
chooseFile();
|
||||
}
|
||||
} else {
|
||||
setDialogAddEntries(progress, savedInstanceState.getInt(TAG_PROGRESS_MAX));
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
private void setDialogAddEntries(ProgressDialog dialog, int length) {
|
||||
dialog.setMax(length);
|
||||
dialog.setMessage(getString(R.string.import_dialog_adding_entries));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
final ProgressDialog dialog = (ProgressDialog) getDialog();
|
||||
|
||||
outState.putInt(TAG_PROGRESS_MAX, dialog.getMax());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
Dialog dialog = getDialog();
|
||||
// handles https://code.google.com/p/android/issues/detail?id=17423
|
||||
if (dialog != null && getRetainInstance()) {
|
||||
dialog.setDismissMessage(null);
|
||||
}
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public void chooseFile() {
|
||||
Intent intent = new Intent();
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setAction(Intent.ACTION_GET_CONTENT);
|
||||
|
||||
if (info.type.equals(CollectionInfo.Type.CALENDAR)) {
|
||||
intent.setType("text/calendar");
|
||||
} else if (info.type.equals(CollectionInfo.Type.ADDRESS_BOOK)) {
|
||||
intent.setType("text/x-vcard");
|
||||
}
|
||||
|
||||
Intent chooser = Intent.createChooser(
|
||||
intent, getString(R.string.choose_file));
|
||||
try {
|
||||
startActivityForResult(chooser, REQUEST_CODE);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
ImportResult data = new ImportResult();
|
||||
data.e = new Exception("Failed to open file chooser.\nPlease install one.");
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(ResultFragment.newInstance(data), null)
|
||||
.commitAllowingStateLoss();
|
||||
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (data != null) {
|
||||
// Get the URI of the selected file
|
||||
final Uri uri = data.getData();
|
||||
App.log.info("Importing uri = " + uri.toString());
|
||||
try {
|
||||
importFile = new File(com.etesync.syncadapter.utils.FileUtils.getPath(getContext(), uri));
|
||||
|
||||
new Thread(new ImportCalendarsLoader()).start();
|
||||
} catch (Exception e) {
|
||||
App.log.severe("File select error: " + e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
break;
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
public void loadFinished(ImportResult data) {
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(ResultFragment.newInstance(data), null)
|
||||
.commitAllowingStateLoss();
|
||||
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
|
||||
private class ImportCalendarsLoader implements Runnable {
|
||||
private void finishParsingFile(final int length) {
|
||||
if (getActivity() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setDialogAddEntries((ProgressDialog) getDialog(), length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void entryProcessed() {
|
||||
if (getActivity() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final ProgressDialog dialog = (ProgressDialog) getDialog();
|
||||
|
||||
dialog.incrementProgressBy(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final ImportResult result = loadInBackground();
|
||||
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
loadFinished(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ImportResult loadInBackground() {
|
||||
ImportResult result = new ImportResult();
|
||||
|
||||
try {
|
||||
@Cleanup FileInputStream importStream = new FileInputStream(importFile);
|
||||
|
||||
if (info.type.equals(CollectionInfo.Type.CALENDAR)) {
|
||||
final Event[] events = Event.fromStream(importStream, Charsets.UTF_8);
|
||||
|
||||
if (events.length == 0) {
|
||||
App.log.warning("Empty/invalid file.");
|
||||
result.e = new Exception("Empty/invalid file.");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.total = events.length;
|
||||
|
||||
finishParsingFile(events.length);
|
||||
|
||||
ContentProviderClient provider = getContext().getContentResolver().acquireContentProviderClient(CalendarContract.CONTENT_URI);
|
||||
LocalCalendar localCalendar;
|
||||
try {
|
||||
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory.INSTANCE, info.url);
|
||||
if (localCalendar == null) {
|
||||
throw new FileNotFoundException("Failed to load local resource.");
|
||||
}
|
||||
} catch (CalendarStorageException | FileNotFoundException e) {
|
||||
App.log.info("Fail" + e.getLocalizedMessage());
|
||||
result.e = e;
|
||||
return result;
|
||||
}
|
||||
|
||||
for (Event event : events) {
|
||||
try {
|
||||
LocalEvent localEvent = localCalendar.getByUid(event.uid);
|
||||
if (localEvent != null) {
|
||||
localEvent.updateAsDirty(event);
|
||||
result.updated++;
|
||||
} else {
|
||||
localEvent = new LocalEvent(localCalendar, event, event.uid, null);
|
||||
localEvent.addAsDirty();
|
||||
result.added++;
|
||||
}
|
||||
} catch (CalendarStorageException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
entryProcessed();
|
||||
}
|
||||
} else if (info.type.equals(CollectionInfo.Type.ADDRESS_BOOK)) {
|
||||
// FIXME: Handle groups and download icon?
|
||||
final Contact[] contacts = Contact.fromStream(importStream, Charsets.UTF_8, null);
|
||||
|
||||
if (contacts.length == 0) {
|
||||
App.log.warning("Empty/invalid file.");
|
||||
result.e = new Exception("Empty/invalid file.");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.total = contacts.length;
|
||||
|
||||
finishParsingFile(contacts.length);
|
||||
|
||||
ContentProviderClient provider = getContext().getContentResolver().acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI);
|
||||
LocalAddressBook localAddressBook = new LocalAddressBook(account, provider);
|
||||
|
||||
for (Contact contact : contacts) {
|
||||
try {
|
||||
LocalContact localContact = (LocalContact) localAddressBook.getByUid(contact.uid);
|
||||
if (localContact != null) {
|
||||
localContact.updateAsDirty(contact);
|
||||
result.updated++;
|
||||
} else {
|
||||
localContact = new LocalContact(localAddressBook, contact, contact.uid, null);
|
||||
localContact.createAsDirty();
|
||||
result.added++;
|
||||
}
|
||||
} catch (ContactsStorageException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
entryProcessed();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (FileNotFoundException e) {
|
||||
result.e = e;
|
||||
return result;
|
||||
} catch (InvalidCalendarException | IOException e) {
|
||||
result.e = e;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ToString
|
||||
static class ImportResult implements Serializable {
|
||||
long total;
|
||||
long added;
|
||||
long updated;
|
||||
Exception e;
|
||||
|
||||
boolean isFailed() {
|
||||
return (e != null);
|
||||
}
|
||||
|
||||
long getSkipped() {
|
||||
return total - (added + updated);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResultFragment extends DialogFragment {
|
||||
private static final String KEY_RESULT = "result";
|
||||
private ImportResult result;
|
||||
|
||||
private static ResultFragment newInstance(ImportResult result) {
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(KEY_RESULT, result);
|
||||
ResultFragment fragment = new ResultFragment();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
result = (ImportResult) getArguments().getSerializable(KEY_RESULT);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
int icon;
|
||||
int title;
|
||||
String msg;
|
||||
if (result.isFailed()) {
|
||||
icon = R.drawable.ic_error_dark;
|
||||
title = R.string.import_dialog_failed_title;
|
||||
msg = result.e.getLocalizedMessage();
|
||||
} else {
|
||||
icon = R.drawable.ic_import_export_black;
|
||||
title = R.string.import_dialog_title;
|
||||
msg = getString(R.string.import_dialog_success, result.total, result.added, result.updated, result.getSkipped());
|
||||
}
|
||||
return new AlertDialog.Builder(getActivity())
|
||||
.setTitle(title)
|
||||
.setIcon(icon)
|
||||
.setMessage(msg)
|
||||
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// dismiss
|
||||
}
|
||||
})
|
||||
.create();
|
||||
}
|
||||
}
|
||||
}
|
@ -159,4 +159,10 @@ public class ViewCollectionActivity extends AppCompatActivity {
|
||||
// FIXME: Handle it more gracefully
|
||||
finish();
|
||||
}
|
||||
|
||||
public void onImport(MenuItem item) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(ImportFragment.newInstance(account, info), null)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
158
app/src/main/java/com/etesync/syncadapter/utils/FileUtils.java
Normal file
158
app/src/main/java/com/etesync/syncadapter/utils/FileUtils.java
Normal file
@ -0,0 +1,158 @@
|
||||
package com.etesync.syncadapter.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
/**
|
||||
* Get a file path from a Uri. This will get the the path for Storage Access
|
||||
* Framework Documents, as well as the _data field for the MediaStore and
|
||||
* other file-based ContentProviders.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param uri The Uri to query.
|
||||
* @author paulburke
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public static String getPath(final Context context, final Uri uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
// DocumentProvider
|
||||
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||
}
|
||||
|
||||
// TODO handle non-primary volumes
|
||||
}
|
||||
// DownloadsProvider
|
||||
else if (isDownloadsDocument(uri)) {
|
||||
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
final Uri contentUri = ContentUris.withAppendedId(
|
||||
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
||||
|
||||
return getDataColumn(context, contentUri, null, null);
|
||||
}
|
||||
// MediaProvider
|
||||
else if (isMediaDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
Uri contentUri = null;
|
||||
if ("image".equals(type)) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("video".equals(type)) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("audio".equals(type)) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
final String selection = "_id=?";
|
||||
final String[] selectionArgs = new String[]{
|
||||
split[1]
|
||||
};
|
||||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
}
|
||||
// MediaStore (and general)
|
||||
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
String path = getDataColumn(context, uri, null, null);
|
||||
if (path != null) {
|
||||
File file = new File(path);
|
||||
if (!file.canRead()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
// File
|
||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the data column for this Uri. This is useful for
|
||||
* MediaStore Uris, and other file-based ContentProviders.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param uri The Uri to query.
|
||||
* @param selection (Optional) Filter used in the query.
|
||||
* @param selectionArgs (Optional) Selection arguments used in the query.
|
||||
* @return The value of the _data column, which is typically a file path.
|
||||
*/
|
||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||
String[] selectionArgs) {
|
||||
|
||||
Cursor cursor = null;
|
||||
final String column = "_data";
|
||||
final String[] projection = {
|
||||
column
|
||||
};
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int column_index = cursor.getColumnIndexOrThrow(column);
|
||||
return cursor.getString(column_index);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
return null;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is ExternalStorageProvider.
|
||||
*/
|
||||
public static boolean isExternalStorageDocument(Uri uri) {
|
||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is DownloadsProvider.
|
||||
*/
|
||||
public static boolean isDownloadsDocument(Uri uri) {
|
||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is MediaProvider.
|
||||
*/
|
||||
public static boolean isMediaDocument(Uri uri) {
|
||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||
}
|
||||
}
|
10
app/src/main/res/drawable/ic_import_export_black.xml
Normal file
10
app/src/main/res/drawable/ic_import_export_black.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:alpha="0.54" >
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"/>
|
||||
</vector>
|
@ -14,4 +14,8 @@
|
||||
android:onClick="onEditCollection"
|
||||
app:showAsAction="always"/>
|
||||
|
||||
<item android:title="@string/view_collection_import"
|
||||
android:onClick="onImport"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
</menu>
|
@ -130,6 +130,15 @@
|
||||
<string name="login_encryption_setup_title">Setting up encryption</string>
|
||||
<string name="login_encryption_setup">Please wait, setting up encryption…</string>
|
||||
|
||||
<!-- ImportFragment -->
|
||||
<string name="import_dialog_title">Import</string>
|
||||
<string name="import_dialog_failed_title">Import Failed</string>
|
||||
<string name="import_dialog_loading_file">Loading file (may take a while)...</string>
|
||||
<string name="import_dialog_adding_entries">Adding entries...</string>
|
||||
<string name="import_dialog_success">Processed %1$d entries.\nAdded: %2$d\nChanged: %3$d\nSkipped (failed): %4$d</string>
|
||||
<string name="import_permission_required">Reading storage permission is required for import.</string>
|
||||
<string name="choose_file">Choose file</string>
|
||||
|
||||
<!-- AccountSettingsActivity -->
|
||||
<string name="settings_title">Settings: %s</string>
|
||||
<string name="settings_authentication">Authentication</string>
|
||||
@ -188,6 +197,7 @@
|
||||
<string name="create_collection_display_name_required">Title is required</string>
|
||||
<string name="create_collection_description">Description (optional):</string>
|
||||
<string name="view_collection_edit">Edit</string>
|
||||
<string name="view_collection_import">Import</string>
|
||||
<string name="create_collection_create">Save</string>
|
||||
<string name="delete_collection">Delete</string>
|
||||
<string name="delete_collection_confirm_title">Are you sure?</string>
|
||||
|
Loading…
Reference in New Issue
Block a user