diff --git a/app/build.gradle b/app/build.gradle index c6b36e6d..d8e67294 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 28 + compileSdkVersion 33 defaultConfig { - minSdkVersion 14 - targetSdkVersion 28 - versionCode 32 - versionName '1.3.1' + minSdkVersion 19 + targetSdkVersion 33 + versionCode 33 + versionName '1.4' } // conditionally load keystore.properties and configure release signing config @@ -39,6 +39,10 @@ android { zipAlignEnabled true } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { @@ -56,7 +60,7 @@ dependencies { implementation 'org.greenrobot:eventbus:3.0.0' implementation 'com.jakewharton:butterknife:7.0.1' annotationProcessor 'com.jakewharton:butterknife:7.0.1' - implementation 'com.android.support:design:28.1.1' + implementation 'com.google.android.material:material:1.0.0' implementation 'com.mobeta.android.dslv:library:0.9.0' // Database implementation 'com.github.satyan:sugar:b16e65326a' @@ -73,8 +77,7 @@ dependencies { annotationProcessor 'com.hannesdorfmann.fragmentargs:processor:4.0.0-RC1' implementation "org.parceler:parceler-api:1.1.12" annotationProcessor "org.parceler:parceler:1.1.12" - implementation "org.permissionsdispatcher:permissionsdispatcher:4.7.0" - annotationProcessor "org.permissionsdispatcher:permissionsdispatcher-processor:4.7.0" + implementation 'pub.devrel:easypermissions:3.0.0' // Debug implementation 'com.facebook.stetho:stetho:1.3.1' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d6163b5..a3bc5b89 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,19 +2,16 @@ - + - - - - - + + + android:launchMode="singleTop" + android:exported="true"> @@ -46,7 +44,8 @@ + android:targetActivity=".activities.MainActivity" + android:exported="true"> @@ -72,10 +71,6 @@ android:name="android.support.PARENT_ACTIVITY" android:value="org.ktachibana.cloudemoji.activities.MainActivity" /> - + android:permission="android.permission.BIND_INPUT_METHOD" + android:exported="true"> diff --git a/app/src/main/java/org/ktachibana/cloudemoji/Constants.java b/app/src/main/java/org/ktachibana/cloudemoji/Constants.java index 0044e05b..d4d396b2 100755 --- a/app/src/main/java/org/ktachibana/cloudemoji/Constants.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/Constants.java @@ -1,19 +1,18 @@ package org.ktachibana.cloudemoji; -import android.os.Environment; import androidx.annotation.IntDef; -import java.io.File; - public interface Constants { // Notification int QUICK_TRIGGER_NOTIFICATION_ID = 0; - String QUICK_TRIGGER_NOTIFICATION_DEFAULT_VISIBILITY = "both"; String QUICK_TRIGGER_NOTIFICATION_CHANNEL_ID = "quick_trigger"; // Preferences String PREF_CLOSE_AFTER_COPY = "pref_close_after_copy"; - String PREF_NOTIFICATION_VISIBILITY = "pref_notification_visibility"; + String PREF_NOTIFICATION_LEGACY_VISIBILITY = "pref_notification_legacy_visibility"; + String QUICK_TRIGGER_NOTIFICATION_LEGACY_VISIBILITY_BOTH = "both"; + String QUICK_TRIGGER_NOTIFICATION_LEGACY_VISIBILITY_NO = "no"; + String QUICK_TRIGGER_NOTIFICATION_LEGACY_VISIBILITY_PANEL = "panel"; String PREF_SHOW_AFTER_BOOT_UP = "pref_show_after_boot_up"; String PREF_VERSION = "pref_version"; String PREF_GIT_HUB_RELEASE = "pref_git_hub_release"; @@ -24,8 +23,9 @@ public interface Constants { String PREF_RESTORE_FAV = "pref_restore_fav"; String PREF_IMPORT_PERSONAL_DICT = "pref_import_into_personal_dict"; String PREF_REVOKE_PERSONAL_DICT = "pref_revoke_from_personal_dict"; - String PREF_BEHAVIORS = "pref_behaviors"; String PREF_NOW_ON_TAP = "pref_now_on_tap"; + String PREF_PERSONAL_DICTIONARY = "pref_ime"; + String PREF_SHOW_NOTIFICATION = "pref_show_notification"; // Repository int FORMAT_TYPE_XML = 0; @@ -48,12 +48,6 @@ public interface Constants { int PREFERENCE_REQUEST_CODE = 1; int REPOSITORY_STORE_REQUEST_CODE = 2; - // File - String FAVORITES_BACKUP_FILE_PATH - = Environment.getExternalStorageDirectory().getPath() + File.separator + "ce.json"; - String EXPORT_FILE_PATH - = Environment.getExternalStorageDirectory().getPath() + File.separator + "%s"; - // Hacking String PACKAGE_NAME_ANDROID_SETTINGS = "com.android.settings"; String CLASS_NAME_MANAGE_ASSIST_ACTIVITY = "com.android.settings.Settings$ManageAssistActivity"; diff --git a/app/src/main/java/org/ktachibana/cloudemoji/activities/BootUpDummyActivity.java b/app/src/main/java/org/ktachibana/cloudemoji/activities/BootUpDummyActivity.java deleted file mode 100644 index b1b8c088..00000000 --- a/app/src/main/java/org/ktachibana/cloudemoji/activities/BootUpDummyActivity.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.ktachibana.cloudemoji.activities; - -import android.app.Activity; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; - -import org.ktachibana.cloudemoji.Constants; -import org.ktachibana.cloudemoji.utils.NotificationUtils; - -/** - * Dummy activity that shows notification after boot up - */ -public class BootUpDummyActivity extends Activity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - - // Show notification according to prefs - Boolean showAfterBootUp = preferences.getBoolean(Constants.PREF_SHOW_AFTER_BOOT_UP, true); - String notificationVisibility = preferences.getString(Constants.PREF_NOTIFICATION_VISIBILITY, Constants.QUICK_TRIGGER_NOTIFICATION_DEFAULT_VISIBILITY); - if (showAfterBootUp) { - NotificationUtils.setupNotificationWithPref(this, notificationVisibility); - } - - finish(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - // OS way to kill this activity from background tasks - android.os.Process.killProcess(android.os.Process.myPid()); - } -} diff --git a/app/src/main/java/org/ktachibana/cloudemoji/activities/MainActivity.java b/app/src/main/java/org/ktachibana/cloudemoji/activities/MainActivity.java index 33f09c49..a1ffa85c 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/activities/MainActivity.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/activities/MainActivity.java @@ -1,5 +1,6 @@ package org.ktachibana.cloudemoji.activities; +import android.Manifest; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; @@ -9,11 +10,14 @@ import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.Bundle; + +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import org.apache.commons.io.IOUtils; @@ -34,6 +38,7 @@ import org.ktachibana.cloudemoji.net.VersionCodeCheckerClient; import org.ktachibana.cloudemoji.parsing.SourceParsingException; import org.ktachibana.cloudemoji.parsing.SourceReader; +import org.ktachibana.cloudemoji.utils.CapabilityUtils; import org.ktachibana.cloudemoji.utils.NotificationUtils; import org.parceler.Parcels; @@ -47,12 +52,15 @@ import java.util.List; import butterknife.ButterKnife; +import pub.devrel.easypermissions.AfterPermissionGranted; +import pub.devrel.easypermissions.EasyPermissions; -public class MainActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener { +public class MainActivity extends BaseActivity { public static final String SOURCE_CACHE_TAG = "sourceCache"; private static final String CURRENT_ITEM_TAG = "currentItem"; private LinkedHashMap sourceCache; private int currentItem; + private static final int RC_POST_NOTIFICATIONS = 123; @Override protected void onCreate(Bundle savedInstanceState) { @@ -93,7 +101,7 @@ private void render() { * Put every source into source cache */ private LinkedHashMap initializeSourceCache() { - LinkedHashMap sourceCache = new LinkedHashMap(); + LinkedHashMap sourceCache = new LinkedHashMap<>(); List allRepositories = Repository.listAll(Repository.class); for (Repository repository : allRepositories) { @@ -114,27 +122,18 @@ private LinkedHashMap initializeSourceCache() { return sourceCache; } + @AfterPermissionGranted(RC_POST_NOTIFICATIONS) private void setupNotification() { - NotificationUtils.setupNotificationWithPref(this, mPreferences.getString(Constants.PREF_NOTIFICATION_VISIBILITY, Constants.QUICK_TRIGGER_NOTIFICATION_DEFAULT_VISIBILITY)); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { - if (Constants.PREF_NOTIFICATION_VISIBILITY.equals(key)) { - setupNotification(); + if (CapabilityUtils.doNotNeedRuntimeNotificationPermission()) { + NotificationUtils.setupNotification(this, null); + return; + } + String[] perms = {Manifest.permission.POST_NOTIFICATIONS}; + if (EasyPermissions.hasPermissions(this, perms)) { + NotificationUtils.setupNotification(this, null); + } else { + EasyPermissions.requestPermissions(this, getString(R.string.notification_rationale), RC_POST_NOTIFICATIONS, perms); } - } - - @Override - protected void onResumeFragments() { - super.onResumeFragments(); - mPreferences.unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - protected void onPause() { - super.onPause(); - mPreferences.registerOnSharedPreferenceChangeListener(this); } @Override @@ -231,12 +230,12 @@ private void checkVersionCode(boolean success, int latestVersionCode) { } else { // New version available, show dialog new MaterialDialog.Builder(MainActivity.this) - .title(getString(R.string.new_version_available) + String.format(" (%d)", latestVersionCode)) + .title(String.format(getString(R.string.new_version_available), latestVersionCode)) .positiveText(R.string.go_to_play_store) .negativeText(android.R.string.cancel) - .callback(new MaterialDialog.ButtonCallback() { + .onPositive(new MaterialDialog.SingleButtonCallback() { @Override - public void onPositive(MaterialDialog dialog) { + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { Intent intent = new Intent(); intent.setData(Uri.parse(Constants.PLAY_STORE_URL)); startActivity(intent); @@ -276,6 +275,7 @@ else if (requestCode == Constants.PREFERENCE_REQUEST_CODE) { @Override protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); outState.putParcelable(SOURCE_CACHE_TAG, Parcels.wrap(sourceCache)); outState.putInt(CURRENT_ITEM_TAG, currentItem); } @@ -295,7 +295,6 @@ private void firstTimeCheck() { } } - @SuppressWarnings("unchecked") private void setupDefaultRepoIfNecessary() { if (Repository.listAll(Repository.class).size() != 0) { // If there are already repositories, ignore @@ -370,4 +369,12 @@ private void upgradeFavoriteDatabaseIfExists() { e.printStackTrace(); } } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + // Forward results to EasyPermissions + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); + } } \ No newline at end of file diff --git a/app/src/main/java/org/ktachibana/cloudemoji/activities/RepositoryManagerActivity.java b/app/src/main/java/org/ktachibana/cloudemoji/activities/RepositoryManagerActivity.java index 1711e491..ea0e7730 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/activities/RepositoryManagerActivity.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/activities/RepositoryManagerActivity.java @@ -1,16 +1,20 @@ package org.ktachibana.cloudemoji.activities; +import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.Nullable; + import com.afollestad.materialdialogs.MaterialDialog; import com.melnykov.fab.FloatingActionButton; import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; import org.greenrobot.eventbus.Subscribe; import org.ktachibana.cloudemoji.BaseActivity; import org.ktachibana.cloudemoji.R; @@ -19,11 +23,13 @@ import org.ktachibana.cloudemoji.events.RepositoryBeginEditingEvent; import org.ktachibana.cloudemoji.events.RepositoryDownloadFailedEvent; import org.ktachibana.cloudemoji.events.RepositoryDownloadedEvent; -import org.ktachibana.cloudemoji.events.RepositoryExportedEvent; +import org.ktachibana.cloudemoji.events.RepositoryExportEvent; import org.ktachibana.cloudemoji.events.RepositoryInvalidFormatEvent; import org.ktachibana.cloudemoji.models.disk.Repository; import org.ktachibana.cloudemoji.ui.MultiInputMaterialDialogBuilder; +import java.io.IOException; +import java.io.OutputStream; import java.util.List; import butterknife.Bind; @@ -43,6 +49,10 @@ public class RepositoryManagerActivity extends BaseActivity { FloatingActionButton mFab; private RepositoryListViewAdapter mAdapter; + private static final String EXPORTED_REPO_MIME_TYPE = "application/json"; + private static final int RC_EXPORT_REPOSITORY = 3; + + private String mCurrentExportingJson; @Override public void onCreate(Bundle savedInstanceBundle) { @@ -154,8 +164,35 @@ public void handle(NetworkUnavailableEvent event) { } @Subscribe - public void handle(RepositoryExportedEvent exportedEvent) { - showSnackBar(exportedEvent.getPath()); + public void handle(RepositoryExportEvent exportedEvent) { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(EXPORTED_REPO_MIME_TYPE); + intent.putExtra(Intent.EXTRA_TITLE, exportedEvent.getAlias() + ".json"); + mCurrentExportingJson = exportedEvent.getJson(); + + startActivityForResult(intent, RC_EXPORT_REPOSITORY); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + OutputStream os = null; + try { + if (resultCode == RESULT_OK) { + if (requestCode == RC_EXPORT_REPOSITORY) { + os = getContentResolver().openOutputStream(data.getData()); + IOUtils.write(mCurrentExportingJson, os); + showSnackBar(R.string.exported_repo); + } + } else { + showSnackBar(getString(R.string.fail)); + } + } catch (IOException e) { + showSnackBar(getString(R.string.fail)); + } finally { + IOUtils.closeQuietly(os); + } } @Subscribe diff --git a/app/src/main/java/org/ktachibana/cloudemoji/adapters/FavoriteListViewAdapter.java b/app/src/main/java/org/ktachibana/cloudemoji/adapters/FavoriteListViewAdapter.java index 070b2050..a3e7f550 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/adapters/FavoriteListViewAdapter.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/adapters/FavoriteListViewAdapter.java @@ -15,6 +15,7 @@ import org.ktachibana.cloudemoji.events.FavoriteDeletedEvent; import org.ktachibana.cloudemoji.models.disk.Favorite; import org.ktachibana.cloudemoji.ui.ScrollableEmoticonMaterialDialogBuilder; +import org.ktachibana.cloudemoji.utils.CapabilityUtils; import java.util.List; @@ -63,7 +64,7 @@ public View getView(int i, View view, ViewGroup viewGroup) { // Setup contents viewHolder.emoticon.setText(favorite.getEmoticon()); - if (favorite.getDescription().equals("") && favorite.getShortcut().equals("")) { + if (favorite.getDescription().equals("") && (CapabilityUtils.personalDictionaryUnavailable() || favorite.getShortcut().equals(""))) { viewHolder.description.setVisibility(View.GONE); } else { viewHolder.description.setVisibility(View.VISIBLE); @@ -71,11 +72,16 @@ public View getView(int i, View view, ViewGroup viewGroup) { favorite.getDescription().equals("") ? "(" + mContext.getString(R.string.no_description) + ")" : favorite.getDescription(); - String shortcutText = - favorite.getShortcut().equals("") - ? "(" + mContext.getString(R.string.no_shortcut) + ")" - : favorite.getShortcut(); - viewHolder.description.setText(descriptionText + " -> " + shortcutText); + + if (CapabilityUtils.personalDictionaryUnavailable()) { + viewHolder.description.setText(descriptionText); + } else { + String shortcutText = + favorite.getShortcut().equals("") + ? "(" + mContext.getString(R.string.no_shortcut) + ")" + : favorite.getShortcut(); + viewHolder.description.setText(descriptionText + " -> " + shortcutText); + } } viewHolder.star.setOnClickListener(new View.OnClickListener() { @Override diff --git a/app/src/main/java/org/ktachibana/cloudemoji/adapters/RepositoryListViewAdapter.java b/app/src/main/java/org/ktachibana/cloudemoji/adapters/RepositoryListViewAdapter.java index c5db0a08..389a9687 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/adapters/RepositoryListViewAdapter.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/adapters/RepositoryListViewAdapter.java @@ -18,14 +18,13 @@ import org.ktachibana.cloudemoji.events.RepositoryBeginEditingEvent; import org.ktachibana.cloudemoji.events.RepositoryDownloadFailedEvent; import org.ktachibana.cloudemoji.events.RepositoryDownloadedEvent; -import org.ktachibana.cloudemoji.events.RepositoryExportedEvent; +import org.ktachibana.cloudemoji.events.RepositoryExportEvent; import org.ktachibana.cloudemoji.models.disk.Repository; import org.ktachibana.cloudemoji.models.memory.Source; import org.ktachibana.cloudemoji.net.RepositoryDownloaderClient; import org.ktachibana.cloudemoji.parsing.SourceJsonParser; import org.ktachibana.cloudemoji.parsing.SourceReader; import org.ktachibana.cloudemoji.ui.NonCancelableProgressMaterialDialogBuilder; -import org.ktachibana.cloudemoji.utils.BackupUtils; import java.io.File; import java.util.List; @@ -33,10 +32,11 @@ import butterknife.Bind; import butterknife.ButterKnife; -public class RepositoryListViewAdapter extends BaseBaseAdapter { +public class RepositoryListViewAdapter extends BaseBaseAdapter implements Constants { private List mRepositories; private Context mContext; + public RepositoryListViewAdapter(Context context) { this.mRepositories = Repository.listAll(Repository.class); this.mContext = context; @@ -132,12 +132,7 @@ public void onClick(View v) { // Parse Source String json = new SourceJsonParser().serialize(source); - // Get file and write - String filePath = String.format(Constants.EXPORT_FILE_PATH, item.getAlias() + ".json"); - File exportFile = new File(filePath); - BackupUtils.writeFileToExternalStorage(json, exportFile); - - mBus.post(new RepositoryExportedEvent(filePath)); + mBus.post(new RepositoryExportEvent(json, item.getAlias())); } catch (Exception e) { Log.e(Constants.DEBUG_TAG, e.getLocalizedMessage()); } diff --git a/app/src/main/java/org/ktachibana/cloudemoji/events/RepositoryExportEvent.java b/app/src/main/java/org/ktachibana/cloudemoji/events/RepositoryExportEvent.java new file mode 100644 index 00000000..1e7e08c5 --- /dev/null +++ b/app/src/main/java/org/ktachibana/cloudemoji/events/RepositoryExportEvent.java @@ -0,0 +1,19 @@ +package org.ktachibana.cloudemoji.events; + +public class RepositoryExportEvent { + private String json; + private String alias; + + public RepositoryExportEvent(String json, String alias) { + this.json = json; + this.alias = alias; + } + + public String getJson() { + return json; + } + + public String getAlias() { + return alias; + } +} diff --git a/app/src/main/java/org/ktachibana/cloudemoji/events/RepositoryExportedEvent.java b/app/src/main/java/org/ktachibana/cloudemoji/events/RepositoryExportedEvent.java deleted file mode 100644 index dbdc48c9..00000000 --- a/app/src/main/java/org/ktachibana/cloudemoji/events/RepositoryExportedEvent.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.ktachibana.cloudemoji.events; - -public class RepositoryExportedEvent { - private String path; - - public RepositoryExportedEvent(String path) { - this.path = path; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } -} diff --git a/app/src/main/java/org/ktachibana/cloudemoji/fragments/FavoriteFragment.java b/app/src/main/java/org/ktachibana/cloudemoji/fragments/FavoriteFragment.java index 4747062b..ce2edb30 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/fragments/FavoriteFragment.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/fragments/FavoriteFragment.java @@ -1,15 +1,14 @@ package org.ktachibana.cloudemoji.fragments; +import android.content.Context; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.RelativeLayout; import android.widget.TextView; -import com.afollestad.materialdialogs.MaterialDialog; import com.melnykov.fab.FloatingActionButton; import com.mobeta.android.dslv.DragSortListView; @@ -24,6 +23,7 @@ import org.ktachibana.cloudemoji.models.disk.Favorite; import org.ktachibana.cloudemoji.models.memory.Entry; import org.ktachibana.cloudemoji.ui.MultiInputMaterialDialogBuilder; +import org.ktachibana.cloudemoji.utils.CapabilityUtils; import java.util.List; @@ -44,11 +44,18 @@ public class FavoriteFragment extends BaseFragment { FloatingActionButton mFab; FavoriteListViewAdapter mAdapter; + private Context mContext; public FavoriteFragment() { // Required empty public constructor } + @Override + public void onAttach(Context context) { + super.onAttach(context); + mContext = context; + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -63,59 +70,45 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, mFavoriteListView .setAdapter(mAdapter); mFavoriteListView - .setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, long l) { - Favorite favorite = (Favorite) mAdapter.getItem(i); - Entry entry = new Entry(favorite.getEmoticon(), favorite.getDescription()); - mBus.post(new EntryAddedToHistoryEvent(entry)); - } + .setOnItemClickListener((adapterView, view, i, l) -> { + Favorite favorite = (Favorite) mAdapter.getItem(i); + Entry entry = new Entry(favorite.getEmoticon(), favorite.getDescription()); + mBus.post(new EntryAddedToHistoryEvent(entry)); }); mFab.setImageDrawable(this.getResources().getDrawable(R.drawable.ic_fab_create)); mFab.attachToListView(mFavoriteListView); - mFab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - popupAddFavoriteDialog(); - } - }); + mFab.setOnClickListener(v -> popupAddFavoriteDialog()); return rootView; } private void popupAddFavoriteDialog() { - new MultiInputMaterialDialogBuilder(getActivity()) - .addInput(null, getString(R.string.emoticon), new MultiInputMaterialDialogBuilder.InputValidator() { - @Override - public CharSequence validate(CharSequence input) { - if (TextUtils.isEmpty(input)) { - return getString(R.string.empty_emoticon); - } else { - List favorites = Favorite.listAll(Favorite.class); - for (Favorite favorite : favorites) { - if (TextUtils.equals(favorite.getEmoticon(), input)) { - return getString(R.string.duplicate_emoticon); - } + new MultiInputMaterialDialogBuilder(mContext) + .addInput(null, getString(R.string.emoticon), input -> { + if (TextUtils.isEmpty(input)) { + return getString(R.string.empty_emoticon); + } else { + List favorites = Favorite.listAll(Favorite.class); + for (Favorite favorite : favorites) { + if (TextUtils.equals(favorite.getEmoticon(), input)) { + return getString(R.string.duplicate_emoticon); } - return null; } + return null; } }) .addInput(null, getString(R.string.description)) - .addInput(null, getString(R.string.shortcut)) - .inputs(new MultiInputMaterialDialogBuilder.InputsCallback() { - @Override - public void onInputs(MaterialDialog dialog, List inputs, boolean allInputsValidated) { - if (allInputsValidated) { - String emoticon = inputs.get(0).toString(); - String description = inputs.get(1).toString(); - String shortcut = inputs.get(2).toString(); - - Favorite favorite = new Favorite(emoticon, description, shortcut); - favorite.save(); - - mAdapter.updateFavorites(); - showSnackBar(emoticon + "\n" + getString(R.string.added_to_fav)); - } + .addInput(null, getString(R.string.shortcut), CapabilityUtils.personalDictionaryUnavailable()) + .inputs((dialog, inputs, allInputsValidated) -> { + if (allInputsValidated) { + String emoticon = inputs.get(0).toString(); + String description = inputs.get(1).toString(); + String shortcut = CapabilityUtils.personalDictionaryUnavailable() ? "" : inputs.get(2).toString(); + + Favorite favorite = new Favorite(emoticon, description, shortcut); + favorite.save(); + + mAdapter.updateFavorites(); + showSnackBar(emoticon + "\n" + getString(R.string.added_to_fav)); } }) .title(R.string.add_to_fav) @@ -127,22 +120,19 @@ public void onInputs(MaterialDialog dialog, List inputs, boolean a @Subscribe public void handle(FavoriteBeginEditingEvent event) { final Favorite favorite = event.getFavorite(); - new MultiInputMaterialDialogBuilder(getActivity()) + new MultiInputMaterialDialogBuilder(mContext) .addInput(favorite.getDescription(), getString(R.string.description)) - .addInput(favorite.getShortcut(), getString(R.string.shortcut)) - .inputs(new MultiInputMaterialDialogBuilder.InputsCallback() { - @Override - public void onInputs(MaterialDialog dialog, List inputs, boolean allInputsValidated) { - String description = inputs.get(0).toString(); - String shortcut = inputs.get(1).toString(); - - // Get the new favorite and SAVE - favorite.setDescription(description); - favorite.setShortcut(shortcut); - favorite.save(); + .addInput(favorite.getShortcut(), getString(R.string.shortcut), CapabilityUtils.personalDictionaryUnavailable()) + .inputs((dialog, inputs, allInputsValidated) -> { + String description = inputs.get(0).toString(); + String shortcut = CapabilityUtils.personalDictionaryUnavailable() ? "" : inputs.get(1).toString(); - mAdapter.updateFavorites(); - } + // Get the new favorite and SAVE + favorite.setDescription(description); + favorite.setShortcut(shortcut); + favorite.save(); + + mAdapter.updateFavorites(); }) .title(R.string.edit_favorite) .positiveText(android.R.string.ok) diff --git a/app/src/main/java/org/ktachibana/cloudemoji/fragments/PreferenceFragment.java b/app/src/main/java/org/ktachibana/cloudemoji/fragments/PreferenceFragment.java index e8fbe379..b238b33d 100755 --- a/app/src/main/java/org/ktachibana/cloudemoji/fragments/PreferenceFragment.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/fragments/PreferenceFragment.java @@ -1,21 +1,24 @@ package org.ktachibana.cloudemoji.fragments; -import android.Manifest; +import static android.app.Activity.RESULT_OK; + import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; + +import androidx.preference.CheckBoxPreference; +import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceFragmentCompat; -import com.afollestad.materialdialogs.AlertDialogWrapper; - +import org.apache.commons.io.IOUtils; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.ktachibana.cloudemoji.BuildConfig; @@ -24,26 +27,35 @@ import org.ktachibana.cloudemoji.events.EmptyEvent; import org.ktachibana.cloudemoji.events.ShowSnackBarOnBaseActivityEvent; import org.ktachibana.cloudemoji.utils.BackupUtils; +import org.ktachibana.cloudemoji.utils.CapabilityUtils; +import org.ktachibana.cloudemoji.utils.NotificationUtils; import org.ktachibana.cloudemoji.utils.PersonalDictionaryUtils; -import org.ktachibana.cloudemoji.utils.SystemUtils; -import permissions.dispatcher.NeedsPermission; -import permissions.dispatcher.OnShowRationale; -import permissions.dispatcher.PermissionRequest; -import permissions.dispatcher.RuntimePermissions; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + -@RuntimePermissions public class PreferenceFragment extends PreferenceFragmentCompat { private static final String CLS_ASSIST_ACTIVITY = "org.ktachibana.cloudemoji.activities.AssistActivity"; + private static final String BACKUP_FILE_MIME_TYPE = "application/json"; + private static final String BACKUP_FILENAME = "ce.json"; + private static final int RC_BACKUP = 1; + private static final int RC_RESTORE = 2; + SharedPreferences.OnSharedPreferenceChangeListener mSharedPreferenceChangeListener; SharedPreferences mPreferences; + private Context mContext; private EventBus mBus; + private ContentResolver mContentResolver; @Override public void onAttach(Context context) { super.onAttach(context); mBus = EventBus.getDefault(); mBus.register(this); + mContext = context; + mContentResolver = context.getContentResolver(); } @Override @@ -56,145 +68,121 @@ public void onDetach() { public void handle(EmptyEvent e) { } - @OnShowRationale({Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}) - public void showRationaleForStorage(final PermissionRequest request) { - new AlertDialogWrapper.Builder(getContext()) - .setMessage(R.string.storage_rationale) - .setPositiveButton(R.string.allow, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - request.proceed(); - } - }) - .setNegativeButton(R.string.deny, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - request.cancel(); - } - }) - .show(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - PreferenceFragmentPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults); - } - @Override public void onCreatePreferences(Bundle paramBundle, String rootKey) { // Load the mPreferences from an XML resource addPreferencesFromResource(R.xml.preferences); mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - // Navbar Gesture + // Setup notification settings + syncShowAfterBootUpToNotificationVisibility(null); + Preference notificationLegacyVisibilityPref = findPreference(Constants.PREF_NOTIFICATION_LEGACY_VISIBILITY); + Preference showNotificationPref = findPreference(Constants.PREF_SHOW_NOTIFICATION); + if (CapabilityUtils.notQuickTriggerNotificationLegacyVisibility()) { + notificationLegacyVisibilityPref.setVisible(false); + showNotificationPref.setOnPreferenceChangeListener((preference, newValue) -> { + syncShowAfterBootUpToNotificationVisibility(newValue); + NotificationUtils.setupNotification(mContext, newValue); + return true; + }); + + } else { + showNotificationPref.setVisible(false); + notificationLegacyVisibilityPref.setOnPreferenceChangeListener((preference, newValue) -> { + syncShowAfterBootUpToNotificationVisibility(newValue); + NotificationUtils.setupNotification(mContext, newValue); + return true; + }); + } + + // Now on Tap or Navbar Gesture Preference navbarGesturePref = findPreference(Constants.PREF_NAVBAR_GESTURE); Preference nowOnTapPref = findPreference(Constants.PREF_NOW_ON_TAP); - PreferenceCategory behaviorsPref = (PreferenceCategory) findPreference(Constants.PREF_BEHAVIORS); - if (SystemUtils.belowJellybean()) - navbarGesturePref.setEnabled(false); - if (SystemUtils.belowMarshmallow()) - behaviorsPref.removePreference(nowOnTapPref); - navbarGesturePref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - PackageManager packageManager = getActivity().getPackageManager(); - ComponentName componentName = new ComponentName(getActivity(), CLS_ASSIST_ACTIVITY); - int componentState = newValue.equals(true) ? - PackageManager.COMPONENT_ENABLED_STATE_ENABLED : - PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - packageManager.setComponentEnabledSetting(componentName, componentState, - PackageManager.DONT_KILL_APP); - return true; - } - }); + if (CapabilityUtils.nowOnTapAvailable()) { + navbarGesturePref.setVisible(false); - // Now on Tap - if (SystemUtils.aboveMarshmallow()) { - behaviorsPref.removePreference(navbarGesturePref); - PackageManager packageManager = getActivity().getPackageManager(); + // Now on Tap + PackageManager packageManager = mContext.getPackageManager(); ComponentName componentName = new ComponentName(getActivity(), CLS_ASSIST_ACTIVITY); packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); - } - nowOnTapPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { + nowOnTapPref.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(); // Didn't found it in android.provider.Settings... Just hardcoded it. - ComponentName componentName = new ComponentName( + ComponentName componentName1 = new ComponentName( Constants.PACKAGE_NAME_ANDROID_SETTINGS, Constants.CLASS_NAME_MANAGE_ASSIST_ACTIVITY); - intent.setComponent(componentName); + intent.setComponent(componentName1); startActivity(intent); return true; - } - }); + }); + } else { + nowOnTapPref.setVisible(false); + + // Navbar Gesture + navbarGesturePref.setOnPreferenceChangeListener((preference, newValue) -> { + PackageManager packageManager = mContext.getPackageManager(); + ComponentName componentName = new ComponentName(getActivity(), CLS_ASSIST_ACTIVITY); + int componentState = newValue.equals(true) ? + PackageManager.COMPONENT_ENABLED_STATE_ENABLED : + PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + packageManager.setComponentEnabledSetting(componentName, componentState, + PackageManager.DONT_KILL_APP); + return true; + }); + } - // Import favorites into personal dictionary - Preference importImePref = findPreference(Constants.PREF_IMPORT_PERSONAL_DICT); - importImePref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - int numberAdded = PersonalDictionaryUtils.importAllFavorites(getActivity().getContentResolver()); + PreferenceCategory personalDictionaryPref = (PreferenceCategory) findPreference(Constants.PREF_PERSONAL_DICTIONARY); + if (CapabilityUtils.personalDictionaryUnavailable()) { + personalDictionaryPref.setVisible(false); + } else { + // Import favorites into personal dictionary + Preference importImePref = findPreference(Constants.PREF_IMPORT_PERSONAL_DICT); + importImePref.setOnPreferenceClickListener(preference -> { + int numberAdded = PersonalDictionaryUtils.importAllFavorites(mContext.getContentResolver()); showSnackBar(String.format(getString(R.string.imported_into_personal_dict), numberAdded)); return true; - } - }); + }); - // Revoke favorite from personal dictionary - Preference revokeImePref = findPreference(Constants.PREF_REVOKE_PERSONAL_DICT); - revokeImePref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - int numberRevoked = PersonalDictionaryUtils.revokeAllFavorites(getActivity().getContentResolver()); + // Revoke favorite from personal dictionary + Preference revokeImePref = findPreference(Constants.PREF_REVOKE_PERSONAL_DICT); + revokeImePref.setOnPreferenceClickListener(preference -> { + int numberRevoked = PersonalDictionaryUtils.revokeAllFavorites(mContext.getContentResolver()); showSnackBar(String.format(getString(R.string.revoked_from_personal_dict), numberRevoked)); return true; - } - }); + }); + } // Backup favorites Preference backupPref = findPreference(Constants.PREF_BACKUP_FAV); - backupPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - PreferenceFragmentPermissionsDispatcher.backupFavoritesWithPermissionCheck(PreferenceFragment.this); - return true; - } + backupPref.setOnPreferenceClickListener(preference -> { + backupFavorites(); + return true; }); // Restore favorites Preference restorePref = findPreference(Constants.PREF_RESTORE_FAV); - restorePref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - PreferenceFragmentPermissionsDispatcher.restoreFavoritesWithPermissionCheck(PreferenceFragment.this); - return true; - } + restorePref.setOnPreferenceClickListener(preference -> { + restoreFavorites(); + return true; }); // GitHub Release Preference gitHubReleasePref = findPreference(Constants.PREF_GIT_HUB_RELEASE); - gitHubReleasePref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Intent intent = new Intent(); - intent.setData(Uri.parse(Constants.GIT_HUB_RELEASE_URL)); - startActivity(intent); - return true; - } + gitHubReleasePref.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(); + intent.setData(Uri.parse(Constants.GIT_HUB_RELEASE_URL)); + startActivity(intent); + return true; }); // GitHub Repo Preference gitHubRepoPref = findPreference(Constants.PREF_GIT_HUB_REPO); - gitHubRepoPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Intent intent = new Intent(); - intent.setData(Uri.parse(Constants.GIT_HUB_REPO_URL)); - startActivity(intent); - return true; - } + gitHubRepoPref.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(); + intent.setData(Uri.parse(Constants.GIT_HUB_REPO_URL)); + startActivity(intent); + return true; }); // Version @@ -205,23 +193,94 @@ public boolean onPreferenceClick(Preference preference) { versionPref.setSummary(getString(R.string.version_code) + " " + versionCode); } - @NeedsPermission({Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}) - void backupFavorites() { - boolean success = BackupUtils.backupFavorites(); - if (success) { - showSnackBar(getString(R.string.backed_up_favorites) + ": " + Constants.FAVORITES_BACKUP_FILE_PATH); + /** + * This method sync the state of "show it after boot-up" preference + * to the state of "show notification"/"notification visibility" preference + * + * e.g. if "show notification"/"notification visibility" is false or no + * "show it after boot-up" cannot be true + * hence we programmatically set it to false, and disable it + * otherwise, we enable the "show it after boot-up" preference + * + * @param newVisibleOrVisibility + */ + private void syncShowAfterBootUpToNotificationVisibility(Object newVisibleOrVisibility) { + CheckBoxPreference showAfterBootUpPref = (CheckBoxPreference) findPreference(Constants.PREF_SHOW_AFTER_BOOT_UP); + + if (CapabilityUtils.notQuickTriggerNotificationLegacyVisibility()) { + CheckBoxPreference showNotificationPref = (CheckBoxPreference) findPreference(Constants.PREF_SHOW_NOTIFICATION); + boolean visible = showNotificationPref.isChecked(); + if (newVisibleOrVisibility != null) { + visible = (boolean) newVisibleOrVisibility; + } + if (!visible) { + SharedPreferences.Editor edit = mPreferences.edit(); + edit.putBoolean(Constants.PREF_SHOW_AFTER_BOOT_UP, false); + edit.apply(); + showAfterBootUpPref.setChecked(false); + showAfterBootUpPref.setEnabled(false); + } else { + showAfterBootUpPref.setEnabled(true); + } } else { - showSnackBar(R.string.fail); + ListPreference notificationLegacyVisibilityPref = (ListPreference) findPreference(Constants.PREF_NOTIFICATION_LEGACY_VISIBILITY); + String visibility = notificationLegacyVisibilityPref.getValue(); + if (newVisibleOrVisibility != null) { + visibility = (String) newVisibleOrVisibility; + } + if (Constants.QUICK_TRIGGER_NOTIFICATION_LEGACY_VISIBILITY_NO.equals(visibility)) { + SharedPreferences.Editor edit = mPreferences.edit(); + edit.putBoolean(Constants.PREF_PERSONAL_DICTIONARY, false); + edit.apply(); + showAfterBootUpPref.setChecked(false); + showAfterBootUpPref.setEnabled(false); + } else { + showAfterBootUpPref.setEnabled(true); + } } } - @NeedsPermission({Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}) + void backupFavorites() { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(BACKUP_FILE_MIME_TYPE); + intent.putExtra(Intent.EXTRA_TITLE, BACKUP_FILENAME); + + startActivityForResult(intent, RC_BACKUP); + } + void restoreFavorites() { - boolean success = BackupUtils.restoreFavorites(); - if (success) { - showSnackBar(R.string.restored_favorites); - } else { - showSnackBar(R.string.fail); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(BACKUP_FILE_MIME_TYPE); + + startActivityForResult(intent, RC_RESTORE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + OutputStream os = null; + InputStream is = null; + try { + if (resultCode == RESULT_OK) { + if (requestCode == RC_BACKUP) { + os = mContentResolver.openOutputStream(data.getData()); + BackupUtils.writeFavorites(os); + showSnackBar(getString(R.string.backed_up_favorites)); + } else if (requestCode == RC_RESTORE) { + is = mContentResolver.openInputStream(data.getData()); + BackupUtils.readFavorites(is); + showSnackBar(getString(R.string.restored_favorites)); + } + } else { + showSnackBar(getString(R.string.fail)); + } + } catch (IOException e) { + showSnackBar(getString(R.string.fail)); + } finally { + IOUtils.closeQuietly(os); + IOUtils.closeQuietly(is); } } @@ -234,8 +293,4 @@ public void onDestroy() { private void showSnackBar(String message) { mBus.post(new ShowSnackBarOnBaseActivityEvent(message)); } - - private void showSnackBar(int resId) { - showSnackBar(getString(resId)); - } } diff --git a/app/src/main/java/org/ktachibana/cloudemoji/net/RepositoryDownloaderClient.java b/app/src/main/java/org/ktachibana/cloudemoji/net/RepositoryDownloaderClient.java index 5b292ec0..0dfa4c52 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/net/RepositoryDownloaderClient.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/net/RepositoryDownloaderClient.java @@ -5,18 +5,17 @@ import com.loopj.android.http.AsyncHttpResponseHandler; import org.apache.commons.io.IOUtils; -import org.apache.http.Header; import org.ktachibana.cloudemoji.BaseApplication; import org.ktachibana.cloudemoji.BaseHttpClient; import org.ktachibana.cloudemoji.models.disk.Repository; -import org.ktachibana.cloudemoji.utils.SystemUtils; +import org.ktachibana.cloudemoji.utils.NetworkUtils; import java.io.File; import java.io.FileOutputStream; public class RepositoryDownloaderClient extends BaseHttpClient { public void downloadSource(@NonNull final Repository item, @NonNull final ObjectCallback callback) { - if (SystemUtils.networkAvailable()) { + if (NetworkUtils.networkAvailable()) { mClient.get( item.getUrl(), new AsyncHttpResponseHandler() { diff --git a/app/src/main/java/org/ktachibana/cloudemoji/net/RepositoryStoreDownloaderClient.java b/app/src/main/java/org/ktachibana/cloudemoji/net/RepositoryStoreDownloaderClient.java index 0829aad8..8e3da468 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/net/RepositoryStoreDownloaderClient.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/net/RepositoryStoreDownloaderClient.java @@ -10,7 +10,7 @@ import org.ktachibana.cloudemoji.BaseHttpClient; import org.ktachibana.cloudemoji.Constants; import org.ktachibana.cloudemoji.models.memory.StoreRepository; -import org.ktachibana.cloudemoji.utils.SystemUtils; +import org.ktachibana.cloudemoji.utils.NetworkUtils; import java.util.ArrayList; import java.util.List; @@ -19,7 +19,7 @@ public class RepositoryStoreDownloaderClient extends BaseHttpClient { public void downloadRepositoryStore(@NonNull final ListCallback callback) { - if (SystemUtils.networkAvailable()) { + if (NetworkUtils.networkAvailable()) { mClient.get(Constants.STORE_URL, new JsonHttpResponseHandler() { public void onSuccess(int statusCode, Header[] headers, JSONArray response) { List repositories = new ArrayList<>(); diff --git a/app/src/main/java/org/ktachibana/cloudemoji/net/VersionCodeCheckerClient.java b/app/src/main/java/org/ktachibana/cloudemoji/net/VersionCodeCheckerClient.java index 9b5daa4a..24efb38f 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/net/VersionCodeCheckerClient.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/net/VersionCodeCheckerClient.java @@ -8,13 +8,13 @@ import org.json.JSONObject; import org.ktachibana.cloudemoji.BaseHttpClient; import org.ktachibana.cloudemoji.Constants; -import org.ktachibana.cloudemoji.utils.SystemUtils; +import org.ktachibana.cloudemoji.utils.NetworkUtils; import cz.msebera.android.httpclient.Header; public class VersionCodeCheckerClient extends BaseHttpClient { public void checkForLatestVersionCode(@NonNull final IntCallback callback) { - if (SystemUtils.networkAvailable()) { + if (NetworkUtils.networkAvailable()) { mClient.get(Constants.UPDATE_CHECKER_URL, new JsonHttpResponseHandler() { public void onSuccess(int statusCode, Header[] headers, JSONObject response) { try { diff --git a/app/src/main/java/org/ktachibana/cloudemoji/parsing/SourceJsonParser.java b/app/src/main/java/org/ktachibana/cloudemoji/parsing/SourceJsonParser.java index 8cb2da0a..9755ea53 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/parsing/SourceJsonParser.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/parsing/SourceJsonParser.java @@ -12,6 +12,10 @@ public class SourceJsonParser { public Source parse(String alias, Reader reader) throws IOException { String json = IOUtils.toString(reader); + return parse(alias, json); + } + + public Source parse(String alias, String json) throws IOException { Source newSource = new Gson().fromJson(json, Source.class); newSource.setAlias(alias); return newSource; diff --git a/app/src/main/java/org/ktachibana/cloudemoji/receivers/BootUpReceiver.java b/app/src/main/java/org/ktachibana/cloudemoji/receivers/BootUpReceiver.java index 0643b032..7b173771 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/receivers/BootUpReceiver.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/receivers/BootUpReceiver.java @@ -1,10 +1,17 @@ package org.ktachibana.cloudemoji.receivers; +import android.Manifest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; -import org.ktachibana.cloudemoji.activities.BootUpDummyActivity; +import org.ktachibana.cloudemoji.Constants; +import org.ktachibana.cloudemoji.utils.CapabilityUtils; +import org.ktachibana.cloudemoji.utils.NotificationUtils; + +import pub.devrel.easypermissions.EasyPermissions; /** * Shows notification when booted up @@ -12,11 +19,25 @@ public class BootUpReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if ("android.intent.action.BOOT_COMPLETED".equals(intent.getAction())) { - Intent dummyActivityIntent = new Intent(context, BootUpDummyActivity.class); - dummyActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - dummyActivityIntent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - context.startActivity(dummyActivityIntent); + if (!"android.intent.action.BOOT_COMPLETED".equals(intent.getAction())) { + return; + } + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + + // Show notification according to prefs + boolean showAfterBootUp = preferences.getBoolean(Constants.PREF_SHOW_AFTER_BOOT_UP, true); + if (!showAfterBootUp) { + return; + } + + if (CapabilityUtils.doNotNeedRuntimeNotificationPermission()) { + NotificationUtils.setupNotification(context, null); + return; + } + String[] perms = {Manifest.permission.POST_NOTIFICATIONS}; + if (EasyPermissions.hasPermissions(context, perms)) { + NotificationUtils.setupNotification(context, null); } } } diff --git a/app/src/main/java/org/ktachibana/cloudemoji/ui/MultiInputMaterialDialogBuilder.java b/app/src/main/java/org/ktachibana/cloudemoji/ui/MultiInputMaterialDialogBuilder.java index 1a4aa1ad..3e61b5e5 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/ui/MultiInputMaterialDialogBuilder.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/ui/MultiInputMaterialDialogBuilder.java @@ -53,6 +53,18 @@ public CharSequence validate(CharSequence input) { }); } + public MultiInputMaterialDialogBuilder addInput(CharSequence preFill, CharSequence hint, boolean optional) { + if (optional) { + return this; + } + return addInput(preFill, hint, new InputValidator() { + @Override + public CharSequence validate(CharSequence input) { + return null; + } + }); + } + public MultiInputMaterialDialogBuilder addInput(@StringRes int preFill, @StringRes int hint, @NonNull InputValidator validator) { return addInput( preFill == 0 ? null : mContext.getString(preFill), diff --git a/app/src/main/java/org/ktachibana/cloudemoji/utils/BackupUtils.java b/app/src/main/java/org/ktachibana/cloudemoji/utils/BackupUtils.java index a51e1b72..8a5a9f24 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/utils/BackupUtils.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/utils/BackupUtils.java @@ -1,110 +1,47 @@ package org.ktachibana.cloudemoji.utils; -import android.os.Environment; - import org.apache.commons.io.IOUtils; -import org.ktachibana.cloudemoji.Constants; import org.ktachibana.cloudemoji.models.disk.Favorite; import org.ktachibana.cloudemoji.models.memory.Entry; import org.ktachibana.cloudemoji.models.memory.Source; import org.ktachibana.cloudemoji.parsing.SourceJsonParser; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileReader; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; public class BackupUtils { - - public static boolean backupFavorites() { - // If external storage not writable - if (!isExternalStorageWritable()) { - return false; - } - - // Get backup file - File backupFile = new File(Constants.FAVORITES_BACKUP_FILE_PATH); - if (backupFile.exists()) backupFile.delete(); - - // Write to file - try { - String json = new SourceJsonParser().serialize(FavoritesUtils.getFavoritesAsSource()); - writeFileToExternalStorage(json, backupFile); - } catch (IOException e) { - return false; - } - return true; - } - - public static boolean restoreFavorites() { - // If external storage not readable - if (!isExternalStorageReadable()) { - return false; + public static void readFavorites(InputStream is) throws IOException { + // Get backed up favorites + String json = IOUtils.toString(is); + Source source = new SourceJsonParser().parse(null, json); + List backedUpFavorites = source.getCategories().get(0).getEntries(); + + // Get current favorites + List favorites = Favorite.listAll(Favorite.class); + List currentFavorites = new ArrayList<>(); + for (Favorite favorite : favorites) { + currentFavorites.add(new Entry(favorite.getEmoticon(), favorite.getDescription())); } - // Get backup file - File backupFile = new File(Constants.FAVORITES_BACKUP_FILE_PATH); - FileReader fileReader = null; - try { - // Get backed up favorites - fileReader = new FileReader(backupFile); - Source source = new SourceJsonParser().parse(null, fileReader); - List backedUpFavorites = source.getCategories().get(0).getEntries(); + // Merge backed up and current favorites + Set mergedFavorites = new HashSet<>(backedUpFavorites); + mergedFavorites.addAll(currentFavorites); - // Get current favorites - List favorites = Favorite.listAll(Favorite.class); - List currentFavorites = new ArrayList(); - for (Favorite favorite : favorites) { - currentFavorites.add(new Entry(favorite.getEmoticon(), favorite.getDescription())); - } - - // Merge backed up and current favorites - Set mergedFavorites = new HashSet(); - for (Entry backedUp : backedUpFavorites) { - mergedFavorites.add(backedUp); - } - for (Entry current : currentFavorites) { - mergedFavorites.add(current); - } - - // Remove all current favorites and add back merged favorites - Favorite.deleteAll(Favorite.class); - for (Entry entry : mergedFavorites) { - Favorite favorite = new Favorite(entry.getEmoticon(), entry.getDescription(), ""); - favorite.save(); - } - } catch (IOException e) { - return false; - } finally { - IOUtils.closeQuietly(fileReader); - } - return true; - } - - public static void writeFileToExternalStorage(String string, File file) throws IOException { - // If external storage not writable - if (!isExternalStorageWritable()) { - return; + // Remove all current favorites and add back merged favorites + Favorite.deleteAll(Favorite.class); + for (Entry entry : mergedFavorites) { + Favorite favorite = new Favorite(entry.getEmoticon(), entry.getDescription(), ""); + favorite.save(); } - - // Write to file - FileOutputStream outputStream = new FileOutputStream(file); - IOUtils.write(string, outputStream); - IOUtils.closeQuietly(outputStream); - } - - private static boolean isExternalStorageWritable() { - String state = Environment.getExternalStorageState(); - return Environment.MEDIA_MOUNTED.equals(state); } - private static boolean isExternalStorageReadable() { - String state = Environment.getExternalStorageState(); - return Environment.MEDIA_MOUNTED.equals(state) - || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state); + public static void writeFavorites(OutputStream os) throws IOException { + String json = new SourceJsonParser().serialize(FavoritesUtils.getFavoritesAsSource()); + IOUtils.write(json, os); } } diff --git a/app/src/main/java/org/ktachibana/cloudemoji/utils/CapabilityUtils.java b/app/src/main/java/org/ktachibana/cloudemoji/utils/CapabilityUtils.java new file mode 100644 index 00000000..b6554516 --- /dev/null +++ b/app/src/main/java/org/ktachibana/cloudemoji/utils/CapabilityUtils.java @@ -0,0 +1,33 @@ +package org.ktachibana.cloudemoji.utils; + +import android.os.Build; + +public class CapabilityUtils { + public static boolean personalDictionaryUnavailable() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + } + + public static boolean cannotGetActiveNotifications() { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M; + } + + public static boolean nowOnTapAvailable() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + } + + public static boolean notQuickTriggerNotificationLegacyVisibility() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; + } + + public static boolean needNotificationChannel() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + } + + public static boolean needPendingIntentMutability() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; + } + + public static boolean doNotNeedRuntimeNotificationPermission() { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU; + } +} diff --git a/app/src/main/java/org/ktachibana/cloudemoji/utils/CopyUtils.java b/app/src/main/java/org/ktachibana/cloudemoji/utils/CopyUtils.java index 7010b0f5..f349c25a 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/utils/CopyUtils.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/utils/CopyUtils.java @@ -1,20 +1,12 @@ package org.ktachibana.cloudemoji.utils; -import android.annotation.TargetApi; import android.content.Context; public class CopyUtils { - @TargetApi(11) public static void copyToClipboard(Context context, String copied) { - if (SystemUtils.belowHoneycomb()) { - android.text.ClipboardManager clipboard - = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(copied); - } else { - android.content.ClipboardManager clipboard - = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("emoji", copied); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard + = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("emoji", copied); + clipboard.setPrimaryClip(clip); } } diff --git a/app/src/main/java/org/ktachibana/cloudemoji/utils/NetworkUtils.java b/app/src/main/java/org/ktachibana/cloudemoji/utils/NetworkUtils.java new file mode 100755 index 00000000..b658f735 --- /dev/null +++ b/app/src/main/java/org/ktachibana/cloudemoji/utils/NetworkUtils.java @@ -0,0 +1,18 @@ +package org.ktachibana.cloudemoji.utils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import org.ktachibana.cloudemoji.BaseApplication; + +public class NetworkUtils { + public static boolean networkAvailable() { + Context context = BaseApplication.context(); + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + if (info == null) return false; + NetworkInfo.State network = info.getState(); + return (network == NetworkInfo.State.CONNECTED || network == NetworkInfo.State.CONNECTING); + } +} diff --git a/app/src/main/java/org/ktachibana/cloudemoji/utils/NotificationUtils.java b/app/src/main/java/org/ktachibana/cloudemoji/utils/NotificationUtils.java index 53287bcb..78bcefa9 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/utils/NotificationUtils.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/utils/NotificationUtils.java @@ -1,12 +1,16 @@ package org.ktachibana.cloudemoji.utils; +import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.os.Build; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.service.notification.StatusBarNotification; + import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; @@ -15,43 +19,95 @@ import org.ktachibana.cloudemoji.activities.MainActivity; /** - * Abstract Notification codes from MainActivity to be shared among MainActivity and BootUpDummyActivity + * Abstract Notification codes from MainActivity to be shared among MainActivity and BootUpReceiver */ -public class NotificationUtils { - +public class NotificationUtils implements Constants { /** * Setup notification with user preference */ - public static void setupNotificationWithPref(Context context, String notificationVisibility) { + public static void setupNotification(Context context, Object newVisibleOrVisibility) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + + if (CapabilityUtils.notQuickTriggerNotificationLegacyVisibility()) { + boolean visible = preferences.getBoolean(Constants.PREF_SHOW_NOTIFICATION, true); + if (newVisibleOrVisibility != null) { + visible = (boolean) newVisibleOrVisibility; + } + if (visible) { + setupLegacyVisibilityBoth(context); + } else { + setupLegacyVisibilityNo(context); + } + } else { + String legacyVisibility = preferences.getString( + Constants.PREF_NOTIFICATION_LEGACY_VISIBILITY, + Constants.QUICK_TRIGGER_NOTIFICATION_LEGACY_VISIBILITY_BOTH + ); + if (newVisibleOrVisibility != null) { + legacyVisibility = (String) newVisibleOrVisibility; + } + switch (legacyVisibility) { + case QUICK_TRIGGER_NOTIFICATION_LEGACY_VISIBILITY_NO: + setupLegacyVisibilityNo(context); + break; + + case QUICK_TRIGGER_NOTIFICATION_LEGACY_VISIBILITY_PANEL: + setupLegacyVisibilityPanel(context); + break; + + case QUICK_TRIGGER_NOTIFICATION_LEGACY_VISIBILITY_BOTH: + setupLegacyVisibilityBoth(context); + break; + } + } + } + + private static void setupLegacyVisibilityNo(Context context) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(Constants.QUICK_TRIGGER_NOTIFICATION_ID); + } - // Cancel current notification + private static void setupLegacyVisibilityPanel(Context context) { + if (doesQuickTriggerNotificationExist(context)) { + return; + } + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(Constants.QUICK_TRIGGER_NOTIFICATION_ID); + createQuickTriggerNotificationChannel(context, NotificationManagerCompat.IMPORTANCE_MIN); + showQuickTriggerNotification(context, NotificationCompat.PRIORITY_MIN); + } - // If not showing - if (notificationVisibility.equals("no")) { - notificationManager.cancel(Constants.QUICK_TRIGGER_NOTIFICATION_ID); + private static void setupLegacyVisibilityBoth(Context context) { + if (doesQuickTriggerNotificationExist(context)) { + return; } + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(Constants.QUICK_TRIGGER_NOTIFICATION_ID); + createQuickTriggerNotificationChannel(context, NotificationManagerCompat.IMPORTANCE_DEFAULT); + showQuickTriggerNotification(context, NotificationCompat.PRIORITY_DEFAULT); + } + + private static boolean doesQuickTriggerNotificationExist(Context context) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - // If only shows in panel - else if (notificationVisibility.equals("panel")) { - createQuickTriggerNotificationChannel(context, notificationManager, NotificationManagerCompat.IMPORTANCE_MIN); - showQuickTriggerNotification(context, notificationManager, NotificationCompat.PRIORITY_MIN); + if (CapabilityUtils.cannotGetActiveNotifications()) { + return false; } - // If shows on both panel and status bar - else if (notificationVisibility.equals("both")) { - createQuickTriggerNotificationChannel(context, notificationManager, NotificationManagerCompat.IMPORTANCE_DEFAULT); - showQuickTriggerNotification(context, notificationManager, NotificationCompat.PRIORITY_DEFAULT); + StatusBarNotification[] notifications = notificationManager.getActiveNotifications(); + for (StatusBarNotification notification : notifications) { + if (notification.getId() == Constants.QUICK_TRIGGER_NOTIFICATION_ID) { + return true; + } } + return false; } private static void createQuickTriggerNotificationChannel( Context context, - NotificationManager notificationManager, int importance ) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (CapabilityUtils.needNotificationChannel()) { NotificationChannel channel = new NotificationChannel( Constants.QUICK_TRIGGER_NOTIFICATION_CHANNEL_ID, context.getString(R.string.quick_trigger_notification_channel_name), @@ -59,19 +115,27 @@ private static void createQuickTriggerNotificationChannel( ); channel.setDescription(context.getString(R.string.quick_trigger_notification_channel_description)); channel.setShowBadge(false); + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.createNotificationChannel(channel); } } + @SuppressLint("UnspecifiedImmutableFlag") private static void showQuickTriggerNotification( Context context, - NotificationManager notificationManager, int priority ) { String title = context.getString(R.string.app_name); String text = context.getString(R.string.touch_to_launch); Intent intent = new Intent(context, MainActivity.class); - PendingIntent pIntent = PendingIntent.getActivity(context, 0, intent, 0); + PendingIntent pIntent; + if (CapabilityUtils.needPendingIntentMutability()) { + pIntent = PendingIntent.getActivity + (context, 0, intent, PendingIntent.FLAG_MUTABLE); + } else { + pIntent = PendingIntent.getActivity + (context, 0, intent, PendingIntent.FLAG_ONE_SHOT); + } Notification notification = new NotificationCompat.Builder(context, Constants.QUICK_TRIGGER_NOTIFICATION_CHANNEL_ID) .setContentTitle(title) // Title .setContentText(text) // Text @@ -79,8 +143,10 @@ private static void showQuickTriggerNotification( .setContentIntent(pIntent) // Intent to launch this app .setWhen(0) // No time to display .setPriority(priority) // Given priority + .setChannelId(Constants.QUICK_TRIGGER_NOTIFICATION_CHANNEL_ID) .build(); notification.flags = Notification.FLAG_NO_CLEAR; + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(Constants.QUICK_TRIGGER_NOTIFICATION_ID, notification); } } \ No newline at end of file diff --git a/app/src/main/java/org/ktachibana/cloudemoji/utils/PersonalDictionaryUtils.java b/app/src/main/java/org/ktachibana/cloudemoji/utils/PersonalDictionaryUtils.java index e282f720..d8d782da 100644 --- a/app/src/main/java/org/ktachibana/cloudemoji/utils/PersonalDictionaryUtils.java +++ b/app/src/main/java/org/ktachibana/cloudemoji/utils/PersonalDictionaryUtils.java @@ -4,7 +4,6 @@ import android.content.ContentValues; import android.provider.UserDictionary; -import org.ktachibana.cloudemoji.Constants; import org.ktachibana.cloudemoji.models.disk.Favorite; import java.util.List; @@ -19,9 +18,7 @@ public static int importAllFavorites(ContentResolver contentResolver) { if (!favorite.getShortcut().equals("")) { ContentValues newValue = new ContentValues(); newValue.put(UserDictionary.Words.WORD, favorite.getEmoticon()); - if (SystemUtils.aboveJellybean()) { - newValue.put(UserDictionary.Words.SHORTCUT, favorite.getShortcut()); - } + newValue.put(UserDictionary.Words.SHORTCUT, favorite.getShortcut()); contentResolver.insert(UserDictionary.Words.CONTENT_URI, newValue); counter++; } diff --git a/app/src/main/java/org/ktachibana/cloudemoji/utils/SystemUtils.java b/app/src/main/java/org/ktachibana/cloudemoji/utils/SystemUtils.java deleted file mode 100755 index fa381d62..00000000 --- a/app/src/main/java/org/ktachibana/cloudemoji/utils/SystemUtils.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.ktachibana.cloudemoji.utils; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Build; - -import org.ktachibana.cloudemoji.BaseApplication; - -/** - * The notorious trash can - */ -public class SystemUtils { - public static boolean networkAvailable() { - Context context = BaseApplication.context(); - ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo info = cm.getActiveNetworkInfo(); - if (info == null) return false; - NetworkInfo.State network = info.getState(); - return (network == NetworkInfo.State.CONNECTED || network == NetworkInfo.State.CONNECTING); - } - - public static boolean networkUnavailable() { - return !networkAvailable(); - } - - public static boolean belowHoneycomb() { - return Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB; - } - - public static boolean aboveHoneycomb() { - return !belowHoneycomb(); - } - - public static boolean belowJellybean() { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN; - } - - public static boolean aboveJellybean() { - return !belowJellybean(); - } - - public static boolean belowMarshmallow() { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M; - } - - public static boolean aboveMarshmallow() { - return !belowMarshmallow(); - } -} diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 253560a0..afb4a22a 100755 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -9,7 +9,7 @@ 触摸以启动 设置 行为 - 复制之后关闭 + 复制之后关闭 App 源URL 关于 GitHub 源 @@ -19,8 +19,8 @@ 添加到收藏 已添加到收藏 已从收藏中移除 - 通知可见性 - 启动后显示通知 + 快捷启动通知的可见性 + 启动后显示 颜文字 描述 编辑源 @@ -37,7 +37,7 @@ 重复的颜文字 导航栏手势 从导航栏上划以启动 - 长按 Home 键启动 + 长按 Home 键启动 App 请点击打开系统设置,在「辅助应用」中选择 你没有任何收藏 你没有任何历史 @@ -51,14 +51,13 @@ 检查新版本 检查新版本失败 已经是最新版本 - 有新版本辣! + 有新版本辣!(%d) 去 Play Store 看看 没找到 - - + 不显示 - 只在通知面板里显示(Android 3.0 以下及 8.0 以上无效) + 只在通知面板里显示 同时在通知面板和状态栏里显示 个人词典 @@ -71,8 +70,7 @@ 已添加到源 允许 拒绝 - 需要储存权限以备份/恢复收藏 - 收藏已备份到 + 收藏已备份 收藏已恢复 通知 在跑调试/测试版, 屌屌哒! @@ -80,10 +78,14 @@ 想要贡献源吗? 这需要最基础的 GitHub, JSON 以及编程知识。你确定要继续吗? 继续 - 快速打开 - 从通知面板中快速打开 App + 快捷启动通知 + 从通知面板中打开 App 云颜文字输入法 没有收藏 将全部收藏从个人词典中撤出 已将 %d 个收藏从个人词典中撤出 + 需要通知权限,这样的话您可以从通知面板中打开 App + 出错辣!请再试一下 + 源已导出 + 启用快捷启动通知 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c38c06de..2fa43069 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,12 +6,12 @@ Exit Copied to clipboard Bad connection, please retry - ლ(╹◡╹ლ) + Something went wrong, please try again ╮( ̄▽ ̄")╭ Touch to launch Settings Behaviors - Close after copy + Close app after copy Repository URL About GitHub repository @@ -21,8 +21,8 @@ Add to bookmarks Added to bookmarks Removed from bookmarks - Notification visibility - Show notification after boot-up + Quick trigger notification visibility + Show it after boot-up Emoticon Description Edit Repository @@ -39,7 +39,7 @@ Duplicate emoticon Navigation bar gesture Swipe-up from the navigation bar to start - Hold Home button to startup + Hold Home button to launch app Touch to open system settings, then select "Assist app" You don\'t have any bookmarks You don\'t have any history @@ -53,13 +53,13 @@ Check for update Failed to check for update Already latest version - New version is available! + New version is available! (%d) Go to Play Store Result not found - + Don\'t show - Only in notification panel (Only works from Android 3.0 to 7.0) + Only in notification panel Both in notification panel and status bar Personal dictionary @@ -72,8 +72,7 @@ Added to repositories Allow Deny - We need storage permission in order to backup/restore your bookmarks. - Backed up favorites to + Backed up bookmarks Restored bookmarks Notification Hey running debug/beta versions, cool kid? @@ -82,9 +81,12 @@ It is assumed that you have some minimal knowledge on GitHub, JSON and programming. Do you still want to proceed? Proceed Quick trigger - Quickly trigger the app from notification pannel + Quickly trigger the app from notification panel Cloud Emoticon IME No bookmark - Reoke all bookmarks from personal dictionary + Revoke all bookmarks from personal dictionary Revoked %d bookmarks from personal dictionary + We need to put a sticky \"quick trigger\" notification so that you can launch Cloud Emoticon conveniently. + Repo exported + Show quick trigger notification \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 9c32b378..f0637aa4 100755 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -1,14 +1,17 @@ +