I developed some app and it's working fine without any problem. No problem on debug. I created some custom class with the ShareActionProvider class methods for check to user actions from ActionShareProvider menu. When I want to generate APK from Android Studio it gives me this error;
Error:(87) Error: This class should provide a default constructor (a public constructor with no arguments) (com.esmobileinc.vetmapp.ShareActionProvider) [Instantiatable]
There is the ShareActionProvider class;
public class ShareActionProvider extends ActionProvider {
public void setOnShareListener(OnShareListener listener) {
mOnShareListener = listener;
setActivityChooserPolicyIfNeeded();
}
/**
* Listener for the event of selecting a share target.
*/
public interface OnShareTargetSelectedListener {
/**
* Called when a share target has been selected. The client can
* decide whether to perform some action before the sharing is
* actually performed.
* <p>
* <strong>Note:</strong> Modifying the intent is not permitted and
* any changes to the latter will be ignored.
* </p>
* <p>
* <strong>Note:</strong> You should <strong>not</strong> handle the
* intent here. This callback aims to notify the client that a
* sharing is being performed, so the client can update the UI
* if necessary.
* </p>
*
* #param source The source of the notification.
* #param intent The intent for launching the chosen share target.
* #return The return result is ignored. Always return false for consistency.
*/
public boolean onShareTargetSelected(ShareActionProvider source, Intent intent);
}
private OnShareListener mOnShareListener; //also need to add getter and setter
public interface OnShareListener {
/**
* Called when a share target has been selected. The client can
* decide whether to perform some action before the sharing is
* actually performed OR handle the action itself.
*
* #param source The source of the notification.
* #param intent The intent for launching the chosen share target.
* #return Return true if you have handled the intent.
*/
public boolean willHandleShareTarget(ShareActionProvider source, Intent intent);
}
/**
* The default for the maximal number of activities shown in the sub-menu.
*/
private static final int DEFAULT_INITIAL_ACTIVITY_COUNT = 4;
/**
* The the maximum number activities shown in the sub-menu.
*/
private int mMaxShownActivityCount = DEFAULT_INITIAL_ACTIVITY_COUNT;
/**
* Listener for handling menu item clicks.
*/
private final ShareMenuItemOnMenuItemClickListener mOnMenuItemClickListener =
new ShareMenuItemOnMenuItemClickListener();
/**
* The default name for storing share history.
*/
public static final String DEFAULT_SHARE_HISTORY_FILE_NAME = "share_history.xml";
/**
* Context for accessing resources.
*/
private final Context mContext;
/**
* The name of the file with share history data.
*/
private String mShareHistoryFileName = DEFAULT_SHARE_HISTORY_FILE_NAME;
private OnShareTargetSelectedListener mOnShareTargetSelectedListener;
private OnChooseActivityListener mOnChooseActivityListener;
/**
* Creates a new instance.
*
* #param context Context for accessing resources.
*/
public ShareActionProvider(Context context) {
super(context);
mContext = context;
}
/**
* Sets a listener to be notified when a share target has been selected.
* The listener can optionally decide to handle the selection and
* not rely on the default behavior which is to launch the activity.
* <p>
* <strong>Note:</strong> If you choose the backing share history file
* you will still be notified in this callback.
* </p>
* #param listener The listener.
*/
public void setOnShareTargetSelectedListener(OnShareTargetSelectedListener listener) {
mOnShareTargetSelectedListener = listener;
setActivityChooserPolicyIfNeeded();
}
/**
* {#inheritDoc}
*/
#Override
public View onCreateActionView() {
// Create the view and set its data model.
ActivityChooserView activityChooserView = new ActivityChooserView(mContext);
if (!activityChooserView.isInEditMode()) {
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
activityChooserView.setActivityChooserModel(dataModel);
}
// Lookup and set the expand action icon.
TypedValue outTypedValue = new TypedValue();
mContext.getTheme().resolveAttribute(R.attr.actionModeShareDrawable, outTypedValue, true);
Drawable drawable = TintManager.getDrawable(mContext, outTypedValue.resourceId);
activityChooserView.setExpandActivityOverflowButtonDrawable(drawable);
activityChooserView.setProvider(this);
// Set content description.
activityChooserView.setDefaultActionButtonContentDescription(
R.string.abc_shareactionprovider_share_with_application);
activityChooserView.setExpandActivityOverflowButtonContentDescription(
R.string.abc_shareactionprovider_share_with);
return activityChooserView;
}
/**
* {#inheritDoc}
*/
#Override
public boolean hasSubMenu() {
return true;
}
/**
* {#inheritDoc}
*/
#Override
public void onPrepareSubMenu(SubMenu subMenu) {
// Clear since the order of items may change.
subMenu.clear();
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
PackageManager packageManager = mContext.getPackageManager();
final int expandedActivityCount = dataModel.getActivityCount();
final int collapsedActivityCount = Math.min(expandedActivityCount, mMaxShownActivityCount);
// Populate the sub-menu with a sub set of the activities.
for (int i = 0; i < collapsedActivityCount; i++) {
ResolveInfo activity = dataModel.getActivity(i);
subMenu.add(0, i, i, activity.loadLabel(packageManager))
.setIcon(activity.loadIcon(packageManager))
.setOnMenuItemClickListener(mOnMenuItemClickListener);
}
if (collapsedActivityCount < expandedActivityCount) {
// Add a sub-menu for showing all activities as a list item.
SubMenu expandedSubMenu = subMenu.addSubMenu(Menu.NONE, collapsedActivityCount,
collapsedActivityCount,
mContext.getString(R.string.abc_activity_chooser_view_see_all));
for (int i = 0; i < expandedActivityCount; i++) {
ResolveInfo activity = dataModel.getActivity(i);
expandedSubMenu.add(0, i, i, activity.loadLabel(packageManager))
.setIcon(activity.loadIcon(packageManager))
.setOnMenuItemClickListener(mOnMenuItemClickListener);
}
}
}
/**
* Sets the file name of a file for persisting the share history which
* history will be used for ordering share targets. This file will be used
* for all view created by {#link #onCreateActionView()}. Defaults to
* {#link #DEFAULT_SHARE_HISTORY_FILE_NAME}. Set to <code>null</code>
* if share history should not be persisted between sessions.
* <p>
* <strong>Note:</strong> The history file name can be set any time, however
* only the action views created by {#link #onCreateActionView()} after setting
* the file name will be backed by the provided file. Therefore, if you want to
* use different history files for sharing specific types of content, every time
* you change the history file {#link #setShareHistoryFileName(String)} you must
* call {#link android.app.Activity#invalidateOptionsMenu()} to recreate the
* action view. You should <strong>not</strong> call
* {#link android.app.Activity#invalidateOptionsMenu()} from
* {#link android.app.Activity#onCreateOptionsMenu(Menu)}."
* <p>
* <code>
* private void doShare(Intent intent) {
* if (IMAGE.equals(intent.getMimeType())) {
* mShareActionProvider.setHistoryFileName(SHARE_IMAGE_HISTORY_FILE_NAME);
* } else if (TEXT.equals(intent.getMimeType())) {
* mShareActionProvider.setHistoryFileName(SHARE_TEXT_HISTORY_FILE_NAME);
* }
* mShareActionProvider.setIntent(intent);
* invalidateOptionsMenu();
* }
* <code>
*
* #param shareHistoryFile The share history file name.
*/
public void setShareHistoryFileName(String shareHistoryFile) {
mShareHistoryFileName = shareHistoryFile;
setActivityChooserPolicyIfNeeded();
}
/**
* Sets an intent with information about the share action. Here is a
* sample for constructing a share intent:
* <p>
* <pre>
* <code>
* Intent shareIntent = new Intent(Intent.ACTION_SEND);
* shareIntent.setType("image/*");
* Uri uri = Uri.fromFile(new File(getFilesDir(), "foo.jpg"));
* shareIntent.putExtra(Intent.EXTRA_STREAM, uri.toString());
* </pre>
* </code>
* </p>
*
* #param shareIntent The share intent.
*
* #see Intent#ACTION_SEND
* #see Intent#ACTION_SEND_MULTIPLE
*/
public void setShareIntent(Intent shareIntent) {
if (shareIntent != null) {
final String action = shareIntent.getAction();
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
updateIntent(shareIntent);
}
}
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext,
mShareHistoryFileName);
dataModel.setIntent(shareIntent);
}
/**
* Reusable listener for handling share item clicks.
*/
private class ShareMenuItemOnMenuItemClickListener implements OnMenuItemClickListener {
#Override
public boolean onMenuItemClick(MenuItem item) {
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext,
mShareHistoryFileName);
final int itemId = item.getItemId();
Intent launchIntent = dataModel.chooseActivity(itemId);
if (launchIntent != null) {
final String action = launchIntent.getAction();
if (Intent.ACTION_SEND.equals(action) ||
Intent.ACTION_SEND_MULTIPLE.equals(action)) {
updateIntent(launchIntent);
}
mContext.startActivity(launchIntent);
}
return true;
}
}
/**
* Set the activity chooser policy of the model backed by the current
* share history file if needed which is if there is a registered callback.
*/
private void setActivityChooserPolicyIfNeeded() {
if (mOnShareListener == null) {
return;
}
if (mOnChooseActivityListener == null) {
mOnChooseActivityListener = new ShareActivityChooserModelPolicy();
}
ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
dataModel.setOnChooseActivityListener(mOnChooseActivityListener);
}
/**
* Policy that delegates to the {#link OnShareTargetSelectedListener}, if such.
*/
private class ShareActivityChooserModelPolicy implements OnChooseActivityListener {
#Override
public boolean onChooseActivity(ActivityChooserModel host, Intent intent) {
if (mOnShareListener != null) {
boolean result = mOnShareListener.willHandleShareTarget(
ShareActionProvider.this, intent);
return result;
}
return false;
}
}
private void updateIntent(Intent intent) {
if (Build.VERSION.SDK_INT >= 21) {
// If we're on Lollipop, we can open the intent as a document
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
} else {
// Else, we will use the old CLEAR_WHEN_TASK_RESET flag
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
}
}
}
There is a constructor here in my ShareActionProvider class;
public ShareActionProvider(Context context) {
super(context);
mContext = context;
}
Why it gives me this error ? How can I solve this problem. Thank you !
I renamed my custom ActionShareProvider.class to CustomActionShareProvider.class and problem resolved.
Related
I have this issue with Webflux which I don't seem to have a valid solution for: I have an item, T, which I need to process using n Webflux actions. Each action is a function that takes T and returns Mono<T>.
I could execute the actions on the item using flatMapSequential, but the issue is that I want to terminate the sequential actions if prior actions has been failed.
For example, say T = User and we want to support deletion of users. So deletion will require the following actions: "deleting from db" > "delete pictures" > "publish delete message on Kafka".
I must follow these exact steps, and not publish a message to Kafka if the DB deletion has been failed.
However, ATM each action that's being executed is a standalone one, so my "publish to Kafka" action is being executed even when my "remove from db" action fails and throws.
I'd love to understand what am I missing...
My execute method:
public Mono<T> execute(final T item) {
if (actions.isEmpty()) {
LOG.warn("No actions to execute on item {}", item);
return Mono.just(item);
}
return Flux.fromIterable(actions)
.as(this::doBeforeItemApply)
.flatMapSequential(this::applyAction, 1)
.onErrorStop()
.contextWrite(ctx -> ctx.put(getItemClass(), item))
.last()
.then(Mono.just(item));
}
protected Mono<A> applyAction(final A action) {
return Mono.deferContextual(ctx -> applyAction(ctx, action, ctx.get(getItemClass()))
.as(this::doOnApplyError)
.as(this::doAfterItemApply)
.contextWrite(innerCtx -> innerCtx.put(getActionClass(), action))
.then(Mono.just(action)));
}
The actions are being injected using Spring Boot.
So seems like I got to something that works, posting it here in case it'll help anybody in the future:
public interface PipelineAction<T> {
/**
* Invokes the action.
* #param item the object to invoke the action on.
* #param ctx the {#link ContextView} available to the action.
* #return a Mono that completes when the log archive has been handled.
*/
#NotNull
Mono<T> process(#NotNull T item, #NotNull ContextView ctx);
}
#Slf4j
public abstract class AbstractActionsPipelineExecutor<T, A extends PipelineAction<T>>
implements ActionsPipelineExecutor<T> {
private static final int DEFAULT_RETRY_MAX_RETRIES = 5;
private static final Duration DEFAULT_RETRY_MIN_BACKOFF_DURATION = Duration.ofSeconds(10);
protected final List<A> actions;
/**
* Instantiates a simple pipeline of actions.
* #param actions the actions to run.
*/
public AbstractActionsPipelineExecutor(#NotNull final List<A> actions) {
this.actions = actions;
}
/** {#inheritDoc} */
#NotNull
#Override
public Mono<T> execute(#NotNull final T item) {
if (actions.isEmpty()) {
LOG.warn("No actions to execute on item {}", item);
return Mono.just(item);
}
return Flux.deferContextual(contextView ->
Flux.fromIterable(actions)
.concatMap(action -> {
onBeforeApply(item, action);
var result = action.process(item, contextView)
.doOnError(throwable -> onApplyError(item, action, throwable))
.doOnSuccess(ignored -> onAfterApply(item, action));
if (getMaxBackoffRetryAttempts() > 0) {
result = result.retryWhen(Retry.backoff(
getMaxBackoffRetryAttempts(), DEFAULT_RETRY_MIN_BACKOFF_DURATION));
}
return result;
})
.onErrorStop()
)
.contextWrite(ctx -> ctx.put(getItemClass(), item))
.last()
.then(Mono.just(item));
}
/**
* Event handler used before an action is applied to the item.
* #param item the item to apply the action to.
* #param action the action to apply.
*/
protected void onBeforeApply(final T item, final A action) { }
/**
* Event handler used after an action has been applied to the item.
* #param item the item that was applied the action to.
* #param action the action that was applied.
*/
protected void onAfterApply(final T item, final A action) { }
/**
* Event handler used when an error occurs while applying an action to the item.
* #param item the item that was applied the action to.
* #param action the action that was applied.
* #param e the error that occurred.
*/
protected void onApplyError(final T item, final A action, final Throwable e) { }
/**
* Returns the maximum number of times to retry an action before giving up.
* #return the maximum number of times to retry an action before giving up.
*/
protected int getMaxBackoffRetryAttempts() {
return DEFAULT_RETRY_MAX_RETRIES;
}
/**
* Returns the class of the item that this pipeline executor is for.
* #return the class of the item that this pipeline executor is for.
*/
#NotNull
protected Duration getMaxBackoffDuration() {
return DEFAULT_RETRY_MIN_BACKOFF_DURATION;
}
/**
* Gets the {#link Class} of the items that are processed by this pipeline.
* #return the {#link Class} of the items that are processed by this pipeline.
*/
protected abstract Class<T> getItemClass();
/**
* Gets the {#link Class} of the actions that are applied by this pipeline.
* #return the {#link Class} of the actions that are applied by this pipeline.
*/
protected abstract Class<A> getActionClass();
/**
* Performs an action when an error occurs while applying an action to the item.
* #param mono the mono to apply the action to.
* #return the mono after the action has been applied.
*/
#NotNull
private Mono<T> doOnApplyError(final Mono<T> mono) {
return Mono.deferContextual(ctx -> mono.doOnError(e -> {
var item = ctx.get(getItemClass());
var action = ctx.get(getActionClass());
onApplyError(item, action, e);
}));
}
}
The expiration time of a JWT can be set by configuring micronaut.security.token.jwt.generator.access-token-expiration.
Is it possible to have a custom value for individually issued JWT tokens? Searching the documentation I haven't found any useful information except you can replace the BearerTokenRenderer and return a custom response.
public AccessRefreshToken render(Integer expiresIn, String accessToken, #Nullable String refreshToken) {
return new AccessRefreshToken(accessToken, refreshToken, BEARER_TOKEN_TYPE, ***customValue**);
}
Will returning a different value for expires_in work in this case?
According to micronaut documentation - pt 9.3.5
You can replace ClaimGenerator with your own:
ClaimGenerator
Which has a method with parameter Integer expiration
You can replace classes by using #Replaces annotation like described here:
https://micronaut-projects.github.io/micronaut-core/latest/guide/#replaces
I hope that solves your issue
The solution is to extend the JwTClaimsSetGenerator
#Replaces(JWTClaimsSetGenerator.class)
#Singleton
public class CustomClaimsGenerator extends JWTClaimsSetGenerator {
private static final Logger LOG = LoggerFactory.getLogger(CustomClaimsGenerator.class);
private static final String ROLES_KEY = "rolesKey";
private final TokenConfiguration tokenConfiguration;
private final JwtIdGenerator jwtIdGenerator;
private final ClaimsAudienceProvider claimsAudienceProvider;
private final String appName;
/**
* #param tokenConfiguration Token Configuration
* #param jwtIdGenerator Generator which creates unique JWT ID
* #param claimsAudienceProvider Provider which identifies the recipients that the JWT is intended for.
* #param applicationConfiguration The application configuration
*/
public CustomClaimsGenerator(TokenConfiguration tokenConfiguration,#Nullable JwtIdGenerator jwtIdGenerator, #Nullable ClaimsAudienceProvider claimsAudienceProvider, ApplicationConfiguration applicationConfiguration) {
super(tokenConfiguration, jwtIdGenerator, claimsAudienceProvider, applicationConfiguration);
this.tokenConfiguration = tokenConfiguration;
this.jwtIdGenerator = jwtIdGenerator;
this.claimsAudienceProvider = claimsAudienceProvider;
this.appName = applicationConfiguration != null ? applicationConfiguration.getName().orElse(Environment.MICRONAUT) : Environment.MICRONAUT;
}
/**
* #param authentication Authenticated user's representation.
* #param expiration expiration time in seconds
* #return The authentication claims
*/
#Override
public Map<String, Object> generateClaims(Authentication authentication, #Nullable Integer expiration) {
expiration = 1000; //works with this value
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
populateIat(builder);
populateExp(builder, 15);
populateJti(builder);
populateIss(builder);
populateAud(builder);
populateNbf(builder);
populateWithAuthentication(builder, authentication);
if (LOG.isDebugEnabled()) {
LOG.debug("Generated claim set: {}", builder.build().toJSONObject());
}
return builder.build().getClaims();
}
/**
* Populates iss claim.
*
* #param builder The Claims Builder
* #see iss (Issuer) Claim
*/
protected void populateIss(JWTClaimsSet.Builder builder) {
if (appName != null) {
builder.issuer(appName); // iss
}
}
/**
* Populates sub claim.
*
* #param builder The Claims Builder
* #param authentication Authenticated user's representation.
* #see sub (Subject) Claim
*/
protected void populateSub(JWTClaimsSet.Builder builder, Authentication authentication) {
builder.subject(authentication.getName()); // sub
}
/**
* Populates aud claim.
*
* #param builder The Claims Builder
* #see aud (Audience) Claim
*/
protected void populateAud(JWTClaimsSet.Builder builder) {
if (claimsAudienceProvider != null) {
builder.audience(claimsAudienceProvider.audience()); // aud
}
}
/**
* Populates exp claim.
*
* #param builder The Claims Builder
* #param expiration expiration time in seconds
* #see exp (ExpirationTime) Claim
*/
protected void populateExp(JWTClaimsSet.Builder builder, #Nullable Integer expiration) {
if (expiration != null) {
LOG.debug("Setting expiration to {}", expiration);
System.out.print(Date.from(Instant.now().plus(expiration, ChronoUnit.SECONDS)));
builder.expirationTime(Date.from(Instant.now().plus(expiration, ChronoUnit.SECONDS))); // exp
}
}
/**
* Populates nbf claim.
*
* #param builder The Claims Builder
* #see nbf (Not Before) Claim
*/
protected void populateNbf(JWTClaimsSet.Builder builder) {
builder.notBeforeTime(new Date()); // nbf
}
/**
* Populates iat claim.
*
* #param builder The Claims Builder
* #see iat (Issued At) Claim
*/
protected void populateIat(JWTClaimsSet.Builder builder) {
builder.issueTime(new Date()); // iat
}
/**
* Populates jti claim.
*
* #param builder The Claims Builder
* #see jti (JWT ID) Claim
*/
protected void populateJti(JWTClaimsSet.Builder builder) {
if (jwtIdGenerator != null) {
builder.jwtID(jwtIdGenerator.generateJtiClaim()); // jti
}
}
/**
* Populates Claims with Authentication object.
*
* #param builder the Claims Builder
* #param authentication Authenticated user's representation.
*/
protected void populateWithAuthentication(JWTClaimsSet.Builder builder, Authentication authentication) {
populateSub(builder, authentication);
authentication.getAttributes().forEach(builder::claim);
String rolesKey = tokenConfiguration.getRolesName();
if (!rolesKey.equalsIgnoreCase(TokenConfiguration.DEFAULT_ROLES_NAME)) {
builder.claim(ROLES_KEY, rolesKey);
}
builder.claim(rolesKey, authentication.getRoles());
}
/**
* #param oldClaims The old claims to use as a base in the new token generation.
* #param expiration expiration time in seconds
* #return Instance of {#link JWTClaimsSet}
*/
#Override
public Map<String, Object> generateClaimsSet(Map<String, ?> oldClaims, Integer expiration) {
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
List<String> excludedClaims = Arrays.asList(JwtClaims.EXPIRATION_TIME, JwtClaims.ISSUED_AT, JwtClaims.NOT_BEFORE);
for (String k : oldClaims.keySet()
.stream()
.filter(p -> !excludedClaims.contains(p))
.collect(Collectors.toList())) {
builder.claim(k, oldClaims.get(k));
}
populateExp(builder, expiration);
populateIat(builder);
populateNbf(builder);
return builder.build().getClaims();
}
}
I'm making a memo app that uses a RecyclerView to show an ArrayList<Note> of data where Note is a data class for each item, and I save and retrieve them using another class called NotesController (using Gson). In order to update the list, what I do now is reassign a new value to the NotesAdapter (the RecyclerView Adapter) I've set to my list. Here's my code:
NotesController:
public class NotesController {
private ArrayList<Note> notesList;
private String notesPath;
private Gson gson = new Gson();
private Type type = new TypeToken<ArrayList<Note>>() {
}.getType();
public NotesController(String notesPath) {
this.notesPath = notesPath;
if (FileUtil.isExistFile(notesPath)) {
getNotesList();
} else {
createNewList();
}
}
/**
* Creates a new list if it doesn't exist. Internal class use only.
*/
private void createNewList() {
notesList = new ArrayList<>();
saveLatestData();
}
/**
* Reads the saved notes.json file and retrieves the ArrayList of items of class {#link Note}.
* #return An ArrayList<<h>Note</h>> containing all notes saved in file <b>notes.json</b>
*/
public ArrayList<Note> getNotesList() {
String json = FileUtil.readFile(notesPath);
notesList = gson.fromJson(json, type);
return notesList;
}
/**
* Saves latest changes to the list {#linkplain NotesController#notesList} to notes.json file. Internal class use only.
*/
private void saveLatestData() {
String json = gson.toJson(notesList, type);
FileUtil.writeFile(notesPath, json);
}
/**
* Adds an item of type {#link Note} to the list and saves data by calling {#link NotesController#saveLatestData()}.
* #param note The {#link Note} instance to get added.
*/
public void add(Note note) {
notesList.add(0, note);
saveLatestData();
}
/**
* Replaces an existing item with a new one of type {#link Note} in the list {#link NotesController#notesList} and saves data by calling {#link NotesController#saveLatestData()}.
* #param position The position of the item to get replaced.
* #param note The {#link Note} instance to replace the old item.
* #throws ArrayIndexOutOfBoundsException When position is out of {#link NotesController#notesList} range.
*/
public void set(int position, Note note) {
notesList.set(position, note);
saveLatestData();
}
/**
* Gets the {#link Note} item from the specified position.
* #param position The position of the item to return.
* #return The item at the position specified.
* #throws ArrayIndexOutOfBoundsException When position is out of {#link NotesController#notesList} range.
*/
public Note get(int position) {
return notesList.get(position);
}
/**
* Removes the {#link Note} item in the specified position from the list.
* #param position The position of the item to remove.
* #throws ArrayIndexOutOfBoundsException When position is out of {#link NotesController#notesList} range.
*/
public void remove(int position) {
notesList.remove(position);
saveLatestData();
}
/**
* Indexes the notes list for the given text and returns items that contain the query either in the title or the content.
* #param query The text query to search for (low cased).
* #return The notes whose title or content contains the query (all trimmed and low cased).
*/
public ArrayList<Note> search(String query) {
ArrayList<Note> results = new ArrayList<>();
for (Note note: getNotesList()) {
if (note.getTitle().trim().toLowerCase().contains(query.trim().toLowerCase()) || note.getContent().trim().toLowerCase().contains(query.trim().toLowerCase())) {
results.add(note);
}
}
return results;
}
/**
* Simple method to convert many int parameters to an int[] array.
* #param categories The varargs int[] array.
* #return int[] array from parameters.
*/
public int[] categories(int... categories) {
return categories;
}
}
MainActivity: (just the relevant codes)
public class MainActivity extends AppCompatActivity {
private NotesAdapter notesAdapter;
public static NotesController notesController;
private RecyclerView notesRecyclerView;
private String notesDir;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Fabric.with(this, new Answers(), new Crashlytics());
setContentView(R.layout.activity_main);
...
notesDir = ContextCompat.getDataDir(this).getPath() + "/files/notes.json";
notesController = new NotesController(notesDir);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
notesRecyclerView.setLayoutManager(layoutManager);
updateRecyclerView();
notesAdapter.setOnItemActionListener(new NotesAdapter.ActionListener() {
#Override
public void onItemClick(final int position, View v) {
...
}
#Override
public void onItemLongClick(final int position, View v) {
AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this).setTitle("Delete?").setMessage("Just for testing.");
dialog.setPositiveButton("DELETE", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialogInterface, int i) {
notesController.remove(position);
updateRecyclerView();
}
});
dialog.show();
}
});
...
}
...
private void updateRecyclerView() {
notesAdapter = new NotesAdapter(notesController.getNotesList(), getApplicationContext());
notesRecyclerView.setAdapter(notesAdapter);
notesAdapter.notifyDataSetChanged();
}
}
Now looking at updateRecyclerView() method you see that I reassign the Adapter once again with the new data from the NotesController then notify the list that the data changed.
But I need somehow, without eliminating the controller, to make list make deletion animation when I delete (e.g. by long click) or add something (just the default one). And for that, Android RecyclerView Adapter class provides us with notifyItemInserted(int) and notifyItemRemoved(int) but they didn't work in my case (even with removing notifyDataSetChanged() which interrupts these animations).
Please don't suggest me to eliminate the NotesController as it helps accessing notes from different parts of the app easily, and I just need a way for those two insertion and deletion notifying methods to work without problems (any other solution is welcome btw).
Note: Here's my adapter type: public class NotesAdapter extends RecyclerView.Adapter<NotesAdapter.NoteViewHolder>.
You shouldn't create new instance of adapter whenever you want to update items in recyclerview. You should create one instance of adapter and assign it to recyclerview, if you want to add or remove items in recyclerview, you just need to replace data in adapter. I would recommend ListAdapter because it has function submitList(list) which can be easily use to update data.
Also if you want to achieve animations you can use this
I fixed the problem by using -as suggested by Milan Kundacina- the function submitList(list) instead of reassigning the adapter. But as this function comes only with ListAdapter while I'm using Adapter only, I've created my own one as follows (suuuuuper simple):
public class NotesAdapter extends RecyclerView.Adapter<NotesAdapter.NoteViewHolder> {
private ArrayList<Note> notesList;
...
NotesAdapter(ArrayList<Note> notesList) {
super();
this.notesList = notesList;
}
public void submitList(ArrayList<Note> list) {
this.notesList = list;
}
...
}
And used this way to update the list:
// inside MainActivity:
private void notifyListAdd(int position) {
notesAdapter.submitList(notesController.getNotesList());
notesAdapter.notifyItemInserted(position);
}
private void notifyListRemove(int position) {
notesAdapter.submitList(notesController.getNotesList());
notesAdapter.notifyItemRemoved(position);
}
I have these classes:
My problem is that for each class I am having to manually register the node.
/**
* Contains the encoder for messages from the server.
*
* #since 18/08/2018
*/
public abstract class MessageEncoder<T> {
/**
* Register this encoder to a message.
*/
public abstract void register();
/**
* Get the encoded message to send to the client.
*
* #param message The message.
* #return the {#link GamePacket} ready to be sent.
*/
public abstract GamePacket encode(T message);
}
Where < T > is always someClass extends Message.
And here is how a message encoder looks like:
public final class ComponentMessageEncoder extends MessageEncoder<ComponentTextMessage> {
#Override
public void register() {
GameConstants.RELEASE.register(ComponentTextMessage.class, this);
}
#Override
public GamePacket encode(ComponentTextMessage message) {
// TODO Auto-generated method stub
return null;
}
}
As you can see the items in the register method, I have to manually type that for every encoder I make.
Is there a shortcut for this that I can just put in MessageEncoder abstract class instead?
I could not find anything that works here
Edit:
Where register sig. is:
/**
* The encoders.
*/
private final Map<Class<? extends Message>, MessageEncoder<?>> encoders = new HashMap<>();
/**
* Register a encoder to a message.
*
* #param message The message.
* #param encoder The encoder.
*/
public void register(Class<? extends Message> message, MessageEncoder<?> encoder) {
encoders.put(message, encoder);
}
You can do slightly better with:
public abstract class MessageEncoder<T extends Message> {
protected MessageEncoder(Class<? extends T> messageClass) {
GameConstants.RELEASE.register(messageClass, this);
}
/**
* Get the encoded message to send to the client.
*
* #param message The message.
* #return the {#link GamePacket} ready to be sent.
*/
public abstract GamePacket encode(T message);
}
Now subclasses would do:
public final class ComponentMessageEncoder extends MessageEncoder<ComponentTextMessage> {
ComponentMessageEncoder() {
super(ComponentTextMessage.class);
}
// etc as before
}
This cuts down slightly on the repetition, and it allows the compiler to stop you from getting it wrong, so it's something of a win.
I wonder how can i mark my test as #Test like in JUnit, because so far I have to use 'test...' name. It means:
#Test
private void creatingAcc(){
instead of
private void testCreatingAcc(){
Thanks in advance
Yes, you can do it. Follow these steps to achieve it:
Go to your's app build.gradle file and add test dependendencies like below:
androidTestCompile 'com.android.support.test:runner:0.4.1'
androidTestCompile 'com.android.support.test:rules:0.4.1'
androidTestCompile 'com.android.support:support-annotations:24.1.1'
compile 'com.jayway.android.robotium:robotium-solo:5.6.1'
Now your build.gradle should look like:
apply plugin: 'com.android.application'
android {
compileSdkVersion 24
buildToolsVersion "24.0.1"
defaultConfig {
applicationId "com.example.piotr.myapplication"
minSdkVersion 15
targetSdkVersion 21
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.1.1'
compile 'com.android.support:design:24.1.1'
androidTestCompile 'com.android.support.test:runner:0.4.1'
androidTestCompile 'com.android.support.test:rules:0.4.1'
androidTestCompile 'com.android.support:support-annotations:24.1.1'
compile 'com.jayway.android.robotium:robotium-solo:5.6.1'
}
Then you need to go to your androidTest directory and create Java class and name it for example MyActivityTestRule
In this file put code below:
#Beta
public class MyActivityTestRule<T extends Activity> extends UiThreadTestRule {
private static final String TAG = "ActivityInstrumentationRule";
private final Class<T> mActivityClass;
public Instrumentation getInstrumentation() {
return mInstrumentation;
}
private Instrumentation mInstrumentation;
private boolean mInitialTouchMode = false;
private boolean mLaunchActivity = false;
private T mActivity;
/**
* Similar to {#link #MyActivityTestRule(Class, boolean, boolean)} but with "touch mode" disabled.
*
* #param activityClass The activity under test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
* #see MyActivityTestRule#MyActivityTestRule(Class, boolean, boolean)
*/
public MyActivityTestRule(Class<T> activityClass) {
this(activityClass, false);
}
/**
* Similar to {#link #MyActivityTestRule(Class, boolean, boolean)} but defaults to launch the
* activity under test once per
* <code>Test</code> method.
* It is launched before the first
* <code>Before</code>
* method, and terminated after the last
* <code>After</code>
* method.
*
* #param activityClass The activity under test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
* #param initialTouchMode true if the Activity should be placed into "touch mode" when started
* #see MyActivityTestRule#MyActivityTestRule(Class, boolean, boolean)
*/
public MyActivityTestRule(Class<T> activityClass, boolean initialTouchMode) {
this(activityClass, initialTouchMode, true);
}
/**
* Creates an {#link MyActivityTestRule} for the Activity under test.
*
* #param activityClass The activity under test. This must be a class in the instrumentation
* targetPackage specified in the AndroidManifest.xml
* #param initialTouchMode true if the Activity should be placed into "touch mode" when started
* #param launchActivity true if the Activity should be launched once per
* <a href="http://junit.org/javadoc/latest/org/junit/Test.html">
* <code>Test</code></a> method. It will be launched before the first
* <a href="http://junit.sourceforge.net/javadoc/org/junit/Before.html">
* <code>Before</code></a> method, and terminated after the last
* <a href="http://junit.sourceforge.net/javadoc/org/junit/After.html">
* <code>After</code></a> method.
*/
public MyActivityTestRule(Class<T> activityClass, boolean initialTouchMode,
boolean launchActivity) {
mActivityClass = activityClass;
mInitialTouchMode = initialTouchMode;
mLaunchActivity = launchActivity;
mInstrumentation = InstrumentationRegistry.getInstrumentation();
}
/**
* Override this method to set up Intent as if supplied to
* {#link android.content.Context#startActivity}.
* <p>
* The default Intent (if this method returns null or is not overwritten) is:
* action = {#link Intent#ACTION_MAIN}
* flags = {#link Intent#FLAG_ACTIVITY_NEW_TASK}
* All other intent fields are null or empty.
*
* #return The Intent as if supplied to {#link android.content.Context#startActivity}.
*/
protected Intent getActivityIntent() {
return new Intent(Intent.ACTION_MAIN);
}
/**
* Override this method to execute any code that should run before your {#link Activity} is
* created and launched.
* This method is called before each test method, including any method annotated with
* <code>Before</code>.
*/
protected void beforeActivityLaunched() {
// empty by default
}
/**
* Override this method to execute any code that should run after your {#link Activity} is
* launched, but before any test code is run including any method annotated with
* <code>Before</code>.
* <p>
* Prefer
* <code>Before</code>
* over this method. This method should usually not be overwritten directly in tests and only be
* used by subclasses of MyActivityTestRule to get notified when the activity is created and
* visible but test runs.
*/
protected void afterActivityLaunched() {
// empty by default
}
/**
* Override this method to execute any code that should run after your {#link Activity} is
* finished.
* This method is called after each test method, including any method annotated with
* <code>After</code>.
*/
protected void afterActivityFinished() {
// empty by default
}
/**
* #return The activity under test.
*/
public T getActivity() {
if (mActivity == null) {
Log.w(TAG, "Activity wasn't created yet");
}
return mActivity;
}
#Override
public Statement apply(final Statement base, Description description) {
return new ActivityStatement(super.apply(base, description));
}
/**
* Launches the Activity under test.
* <p>
* Don't call this method directly, unless you explicitly requested not to launch the Activity
* manually using the launchActivity flag in
* {#link MyActivityTestRule#MyActivityTestRule(Class, boolean, boolean)}.
* <p>
* Usage:
* <pre>
* #Test
* public void customIntentToStartActivity() {
* Intent intent = new Intent(Intent.ACTION_PICK);
* mActivity = mActivityRule.launchActivity(intent);
* }
* </pre>
* #param startIntent The Intent that will be used to start the Activity under test. If
* {#code startIntent} is null, the Intent returned by
* {#link MyActivityTestRule#getActivityIntent()} is used.
* #return the Activity launched by this rule.
* #see MyActivityTestRule#getActivityIntent()
*/
public T launchActivity(#Nullable Intent startIntent) {
// set initial touch mode
mInstrumentation.setInTouchMode(mInitialTouchMode);
final String targetPackage = mInstrumentation.getTargetContext().getPackageName();
// inject custom intent, if provided
if (null == startIntent) {
startIntent = getActivityIntent();
if (null == startIntent) {
Log.w(TAG, "getActivityIntent() returned null using default: " +
"Intent(Intent.ACTION_MAIN)");
startIntent = new Intent(Intent.ACTION_MAIN);
}
}
startIntent.setClassName(targetPackage, mActivityClass.getName());
startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Log.d(TAG, String.format("Launching activity %s",
mActivityClass.getName()));
beforeActivityLaunched();
// The following cast is correct because the activity we're creating is of the same type as
// the one passed in
mActivity = mActivityClass.cast(mInstrumentation.startActivitySync(startIntent));
mInstrumentation.waitForIdleSync();
afterActivityLaunched();
return mActivity;
}
// Visible for testing
void setInstrumentation(Instrumentation instrumentation) {
mInstrumentation = checkNotNull(instrumentation, "instrumentation cannot be null!");
}
void finishActivity() {
if (mActivity != null) {
mActivity.finish();
mActivity = null;
}
}
/**
* <a href="http://junit.org/apidocs/org/junit/runners/model/Statement.html">
* <code>Statement</code></a> that finishes the activity after the test was executed
*/
private class ActivityStatement extends Statement {
private final Statement mBase;
public ActivityStatement(Statement base) {
mBase = base;
}
#Override
public void evaluate() throws Throwable {
try {
if (mLaunchActivity) {
mActivity = launchActivity(getActivityIntent());
}
mBase.evaluate();
} finally {
finishActivity();
afterActivityFinished();
}
}
}
}
Well, to be honest, it's standard ActivityTestRule with additional getter getInstrumentation(), which I would used in my test class.
Create a new Java class - it would be your test class and put the code below:
#RunWith(AndroidJUnit4.class)
public class MainActivityTest {
private Solo solo;
private static final String MAIN_ACTIVITY = MainActivity.class.getSimpleName();
#Rule
public MyActivityTestRule<MainActivity> mActivityRule = new MyActivityTestRule<>(MainActivity.class);
#Before
public void setUp() throws Exception {
solo = new Solo(mActivityRule.getInstrumentation(), mActivityRule.getActivity());
}
#Test
public void checkIfMainActivityIsProperlyDisplayed() throws InterruptedException {
solo.waitForActivity("MainActivity", 2000);
solo.assertCurrentActivity(mActivityRule.getActivity().getString(
R.string.error_no_class_def_found, MAIN_ACTIVITY), MAIN_ACTIVITY);
solo.getText("Hello World").isShown();
}
}
As you see it's not difficult as might be.
Hope it will help