Import data from Tickmate and Rewire

Closes #36, closes #41
pull/77/merge
Alinson S. Xavier 10 years ago
parent dfe5c4954e
commit 581197be03

@ -27,12 +27,10 @@
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name="HabitsApplication"

@ -19,6 +19,7 @@
package org.isoron.uhabits;
import android.Manifest;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@ -26,10 +27,15 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.view.Menu;
import android.view.MenuItem;
@ -37,6 +43,7 @@ import android.view.MenuItem;
import org.isoron.helpers.DateHelper;
import org.isoron.helpers.DialogHelper;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.dialogs.FilePickerDialog;
import org.isoron.uhabits.fragments.ListHabitsFragment;
import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.models.Habit;
@ -46,6 +53,8 @@ import org.isoron.uhabits.widgets.HistoryWidgetProvider;
import org.isoron.uhabits.widgets.ScoreWidgetProvider;
import org.isoron.uhabits.widgets.StreakWidgetProvider;
import java.io.File;
public class MainActivity extends ReplayableActivity
implements ListHabitsFragment.OnHabitClickListener
{
@ -120,6 +129,12 @@ public class MainActivity extends ReplayableActivity
{
switch (item.getItemId())
{
case R.id.action_import:
{
onActionImportClicked();
return true;
}
case R.id.action_settings:
{
Intent intent = new Intent(this, SettingsActivity.class);
@ -139,6 +154,22 @@ public class MainActivity extends ReplayableActivity
}
}
private void onActionImportClicked()
{
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
PackageManager.PERMISSION_GRANTED)
{
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN)
return;
String[] permissions = new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE };
ActivityCompat.requestPermissions(this, permissions, 0);
return;
}
listHabitsFragment.showImportDialog();
}
@Override
public void onHabitClicked(Habit habit)
{
@ -197,4 +228,14 @@ public class MainActivity extends ReplayableActivity
listHabitsFragment.onPostExecuteCommand(null);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults)
{
if (grantResults.length <= 0) return;
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) return;
listHabitsFragment.showImportDialog();
}
}

@ -0,0 +1,175 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.dialogs;
import android.app.Activity;
import android.app.Dialog;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import java.io.File;
import java.io.FileFilter;
import java.util.Arrays;
public class FilePickerDialog implements AdapterView.OnItemClickListener
{
private static final String PARENT_DIR = "..";
private final Activity activity;
private ListView list;
private Dialog dialog;
private File currentPath;
public interface OnFileSelectedListener
{
void onFileSelected(File file);
}
private OnFileSelectedListener fileListener;
public FilePickerDialog(Activity activity, File initialDirectory)
{
this.activity = activity;
list = new ListView(activity);
list.setOnItemClickListener(this);
dialog = new Dialog(activity);
dialog.setContentView(list);
dialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
navigateTo(initialDirectory);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int which, long id)
{
String filename = (String) list.getItemAtPosition(which);
File file;
if (filename.equals(PARENT_DIR))
file = currentPath.getParentFile();
else
file = new File(currentPath, filename);
if (file.isDirectory())
{
navigateTo(file);
}
else
{
if (fileListener != null) fileListener.onFileSelected(file);
dialog.dismiss();
}
}
public void show()
{
dialog.show();
}
public void setFileListener(OnFileSelectedListener fileListener)
{
this.fileListener = fileListener;
}
private void navigateTo(File path)
{
if (!path.exists()) return;
File[] dirs = path.listFiles(new ReadableDirFilter());
File[] files = path.listFiles(new RegularReadableFileFilter());
if(dirs == null || files == null) return;
this.currentPath = path;
dialog.setTitle(currentPath.getPath());
list.setAdapter(new FilePickerAdapter(getFileList(path, dirs, files)));
}
@NonNull
private String[] getFileList(File path, File[] dirs, File[] files)
{
int count = 0;
int length = dirs.length + files.length;
String[] fileList;
if (path.getParentFile() == null || !path.getParentFile().canRead())
{
fileList = new String[length];
}
else
{
fileList = new String[length + 1];
fileList[count++] = PARENT_DIR;
}
Arrays.sort(dirs);
Arrays.sort(files);
for (File dir : dirs)
fileList[count++] = dir.getName();
for (File file : files)
fileList[count++] = file.getName();
return fileList;
}
private class FilePickerAdapter extends ArrayAdapter<String>
{
public FilePickerAdapter(@NonNull String[] fileList)
{
super(FilePickerDialog.this.activity, android.R.layout.simple_list_item_1, fileList);
}
@Override
public View getView(int pos, View view, ViewGroup parent)
{
view = super.getView(pos, view, parent);
TextView tv = (TextView) view;
tv.setSingleLine(true);
return view;
}
}
private static class ReadableDirFilter implements FileFilter
{
@Override
public boolean accept(File file)
{
return (file.isDirectory() && file.canRead());
}
}
private class RegularReadableFileFilter implements FileFilter
{
@Override
public boolean accept(File file)
{
return !file.isDirectory() && file.canRead();
}
}
}

@ -0,0 +1,109 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.fragments;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.ProgressBar;
import org.isoron.uhabits.io.GenericImporter;
import java.io.File;
import java.io.IOException;
public class ImportHabitsAsyncTask extends AsyncTask<Void, Void, Void>
{
public static final int SUCCESS = 1;
public static final int NOT_RECOGNIZED = 2;
public static final int FAILED = 3;
public interface Listener
{
void onImportFinished(int result);
}
@Nullable
private final ProgressBar progressBar;
@NonNull
private final File file;
@Nullable
private Listener listener;
int result;
public ImportHabitsAsyncTask(@NonNull File file, @Nullable ProgressBar progressBar)
{
this.file = file;
this.progressBar = progressBar;
}
public void setListener(@Nullable Listener listener)
{
this.listener = listener;
}
@Override
protected void onPreExecute()
{
if(progressBar != null)
{
progressBar.setIndeterminate(true);
progressBar.setVisibility(View.VISIBLE);
}
}
@Override
protected void onPostExecute(Void aVoid)
{
if(progressBar != null)
progressBar.setVisibility(View.GONE);
if(listener != null) listener.onImportFinished(result);
}
@Override
protected Void doInBackground(Void... params)
{
try
{
GenericImporter importer = new GenericImporter();
if(importer.canHandle(file))
{
importer.importHabitsFromFile(file);
result = SUCCESS;
}
else
{
result = NOT_RECOGNIZED;
}
}
catch (IOException e)
{
result = FAILED;
e.printStackTrace();
}
return null;
}
}

@ -25,6 +25,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.view.ActionMode;
import android.view.ContextMenu;
@ -55,6 +56,7 @@ import org.isoron.helpers.DialogHelper.OnSavedListener;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.dialogs.FilePickerDialog;
import org.isoron.uhabits.dialogs.HabitSelectionCallback;
import org.isoron.uhabits.dialogs.HintManager;
import org.isoron.uhabits.helpers.ListHabitsHelper;
@ -62,6 +64,7 @@ import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.loaders.HabitListLoader;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
@ -69,7 +72,7 @@ import java.util.List;
public class ListHabitsFragment extends Fragment
implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener,
OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener,
HabitSelectionCallback.Listener
HabitSelectionCallback.Listener, ImportHabitsAsyncTask.Listener
{
long lastLongClick = 0;
private boolean isShortToggleEnabled;
@ -426,4 +429,41 @@ public class ListHabitsFragment extends Fragment
selectItem(position);
}
}
public void showImportDialog()
{
File dir = Environment.getExternalStorageDirectory();
FilePickerDialog picker = new FilePickerDialog(activity, dir);
picker.setFileListener(new FilePickerDialog.OnFileSelectedListener()
{
@Override
public void onFileSelected(File file)
{
ImportHabitsAsyncTask task = new ImportHabitsAsyncTask(file, progressBar);
task.setListener(ListHabitsFragment.this);
task.execute();
}
});
picker.show();
}
@Override
public void onImportFinished(int result)
{
switch (result)
{
case ImportHabitsAsyncTask.SUCCESS:
loader.updateAllHabits(true);
activity.showToast(R.string.habits_imported);
break;
case ImportHabitsAsyncTask.NOT_RECOGNIZED:
activity.showToast(R.string.file_not_recognized);
break;
default:
activity.showToast(R.string.could_not_import);
break;
}
}
}

@ -0,0 +1,48 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.io;
import android.support.annotation.NonNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
public abstract class AbstractImporter
{
public abstract boolean canHandle(@NonNull File file) throws IOException;
public abstract void importHabitsFromFile(@NonNull File file) throws IOException;
public static boolean isSQLite3File(@NonNull File file) throws IOException
{
FileInputStream fis = new FileInputStream(file);
byte[] sqliteHeader = "SQLite format 3".getBytes();
byte[] buffer = new byte[sqliteHeader.length];
int count = fis.read(buffer);
if(count < sqliteHeader.length) return false;
return Arrays.equals(buffer, sqliteHeader);
}
}

@ -0,0 +1,56 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.io;
import android.support.annotation.NonNull;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class GenericImporter extends AbstractImporter
{
List<AbstractImporter> importers;
public GenericImporter()
{
importers = new LinkedList<>();
importers.add(new RewireDBImporter());
importers.add(new TickmateDBImporter());
}
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
for(AbstractImporter importer : importers)
if(importer.canHandle(file)) return true;
return false;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
for(AbstractImporter importer : importers)
if(importer.canHandle(file))
importer.importHabitsFromFile(file);
}
}

@ -0,0 +1,190 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.io;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import org.isoron.helpers.ActiveAndroidHelper;
import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.io.IOException;
import java.util.GregorianCalendar;
public class RewireDBImporter extends AbstractImporter
{
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if(!isSQLite3File(file)) return false;
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"CHECKINS", "UNIT"});
boolean result = (c.moveToFirst() && c.getInt(0) == 2);
c.close();
return result;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command()
{
@Override
public void execute()
{
createHabits(db);
}
});
}
private void createHabits(SQLiteDatabase db)
{
Cursor c = null;
try
{
c = db.rawQuery("select _id, name, description, schedule, active_days, " +
"repeating_count, days, period from habits", new String[0]);
if (!c.moveToFirst()) return;
do
{
int id = c.getInt(0);
String name = c.getString(1);
String description = c.getString(2);
int schedule = c.getInt(3);
String activeDays = c.getString(4);
int repeatingCount = c.getInt(5);
int days = c.getInt(6);
int periodIndex = c.getInt(7);
Habit habit = new Habit();
habit.name = name;
habit.description = description;
int periods[] = { 7, 31, 365 };
switch (schedule)
{
case 0:
habit.freqNum = activeDays.split(",").length;
habit.freqDen = 7;
break;
case 1:
habit.freqNum = days;
habit.freqDen = periods[periodIndex];
break;
case 2:
habit.freqNum = 1;
habit.freqDen = repeatingCount;
break;
}
habit.save();
createReminder(db, habit, id);
createCheckmarks(db, habit, id);
}
while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
private void createReminder(SQLiteDatabase db, Habit habit, int rewireHabitId)
{
String[] params = { Integer.toString(rewireHabitId) };
Cursor c = null;
try
{
c = db.rawQuery("select time, active_days from reminders where habit_id=? limit 1", params);
if (!c.moveToFirst()) return;
int rewireReminder = Integer.parseInt(c.getString(0));
if (rewireReminder <= 0 || rewireReminder >= 1440) return;
boolean reminderDays[] = new boolean[7];
String activeDays[] = c.getString(1).split(",");
for(String d : activeDays)
{
int idx = (Integer.parseInt(d) + 1) % 7;
reminderDays[idx] = true;
}
habit.reminderDays = DateHelper.packWeekdayList(reminderDays);
habit.reminderHour = rewireReminder / 60;
habit.reminderMin = rewireReminder % 60;
habit.save();
}
finally
{
if(c != null) c.close();
}
}
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int rewireHabitId)
{
Cursor c = null;
try
{
String[] params = { Integer.toString(rewireHabitId) };
c = db.rawQuery("select distinct date from checkins where habit_id=? and type=2", params);
if (!c.moveToFirst()) return;
do
{
String date = c.getString(0);
int year = Integer.parseInt(date.substring(0, 4));
int month = Integer.parseInt(date.substring(4, 6));
int day = Integer.parseInt(date.substring(6, 8));
GregorianCalendar cal = DateHelper.getStartOfTodayCalendar();
cal.set(year, month - 1, day);
habit.repetitions.toggle(cal.getTimeInMillis());
}
while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
}

@ -0,0 +1,130 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.io;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import org.isoron.helpers.ActiveAndroidHelper;
import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.io.IOException;
import java.util.GregorianCalendar;
public class TickmateDBImporter extends AbstractImporter
{
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if(!isSQLite3File(file)) return false;
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"tracks", "track2groups"});
boolean result = (c.moveToFirst() && c.getInt(0) == 2);
c.close();
return result;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command()
{
@Override
public void execute()
{
createHabits(db);
}
});
}
private void createHabits(SQLiteDatabase db)
{
Cursor c = null;
try
{
c = db.rawQuery("select _id, name, description from tracks", new String[0]);
if (!c.moveToFirst()) return;
do
{
int id = c.getInt(0);
String name = c.getString(1);
String description = c.getString(2);
Habit habit = new Habit();
habit.name = name;
habit.description = description;
habit.freqNum = 1;
habit.freqDen = 1;
habit.save();
createCheckmarks(db, habit, id);
}
while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int tickmateTrackId)
{
Cursor c = null;
try
{
String[] params = { Integer.toString(tickmateTrackId) };
c = db.rawQuery("select distinct year, month, day from ticks where _track_id=?", params);
if (!c.moveToFirst()) return;
do
{
int year = c.getInt(0);
int month = c.getInt(1);
int day = c.getInt(2);
GregorianCalendar cal = DateHelper.getStartOfTodayCalendar();
cal.set(year, month, day);
habit.repetitions.toggle(cal.getTimeInMillis());
}
while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
}

@ -28,6 +28,12 @@
android:enabled="true"
android:title="@string/show_archived"/>
<item
android:id="@+id/action_import"
android:orderInCategory="50"
android:title="Import data"
app:showAsAction="never"/>
<item
android:id="@+id/action_settings"
android:orderInCategory="100"

@ -139,4 +139,7 @@
<string name="custom_frequency">Custom …</string>
<string name="help">Help &amp; FAQ</string>
<string name="could_not_export">Failed to export data.</string>
<string name="could_not_import">Failed to import habits from file.</string>
<string name="file_not_recognized">File type not recognized.</string>
<string name="habits_imported">Habits imported successfully.</string>
</resources>

@ -40,6 +40,25 @@
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_links"
android:title="Database">
<Preference android:title="Export data">
<intent
android:action="android.intent.action.VIEW"
android:data="@string/helpURL"/>
</Preference>
<Preference android:title="Import data"
android:summary="Supports files exported by Loop, Tickmate, HabitBull or Rewire. This feature is currently experimental.">
<intent
android:action="android.intent.action.VIEW"
android:data="@string/helpURL"/>
</Preference>
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_links"
android:title="@string/links">

Loading…
Cancel
Save